You could always do both.
Provide a low-level abstraction-light API that allows fine control but requires deep expertise, and write a higher-level abstraction-rich API on top of it that maps to fewer simple operations for the most common use cases - which some of your clients might be implementing their own half-baked versions of anyway.
If you maintain a clean separation between the two, having both in place might mean there is less pressure to add abstractions to the low-level API, or to add warts and special-cases to the high-level API. If a client wants one of those things, it already exists - in the other API.
Bonus points for providing materials to help your clients learn how to move from one to the other. You can attract clients who do not yet have deep knowledge of payment network internals, but are looking to improve in that direction.
Just today I was working with the Web File System API, and e.g. just writing a string to a file requires seven function calls, most async. And this doesn't even handle errors. And has to be done in a worker, setting up of which is similar faff in itself. Similar horrors can be seen in e.g. IndexedDB, WebRTC and even plain old vanilla DOM manipulation. And things like Vulkan and DirectX and ffmpeg are even way worse.
The complexity can be largely justified to be able to handle all sorts of exotic cases, but vast majority of cases aren't these exotic ones.
API design should start first by sketching out how using the API looks for common cases, and those should be as simple as possible. E.g. the fetch API does this quite well. XMLHttpRequest definitely did not.
https://developer.mozilla.org/en-US/docs/Web/API/FileSystemS...
Edit: I've thought many a time that there should be some unified "porcelain" API for all the Web APIs. It would wrap all the (awesome) features in one coherent "standard library" wrapper, supporting at least the most common use cases. Modern browsers are very powerful and capable but a lot of this power and capability is largely unknown or underused because each API tends to have quite an idiosyncratic design and they are needlessly hard to learn and/or use.
Something like what jQuery did for DOM (but with less magic and no extra bells and whistles). E.g. node.js has somewhat coherent, but a bit outdated APIs (e.g. Promise support is spotty and inconsistent). Something like how Python strives for "pythonic" APIs (with varying success).
”The purpose of abstraction is not to be vague, but to create a new semantic level in which one can be absolutely precise.”
If only the “A” in API stood for “abstraction”. For many APIs, it probably stands for “accreted”. :)
[0] https://news.ycombinator.com/item?id=40021696
[1] https://youtu.be/GqmsQeSzMdw?t=874 (this takes you right to the Dijkstra quote)
https://developer.mozilla.org/en-US/docs/Web/API/File_System...
It started to break down with programmable shader pipelines. And got a bit weird, with most of the API being irrelevant for the programmable-pipeline hardware.
As for Vulkan, it's a very different type of API. It's a kind of hardware abstraction layer that's easier to write drivers for. You should not use Vulkan if you're not writing hardware drivers or middleware. It's an extremely verbose and cumbersome API to use for anything.
For a low level API it's probably OK to write other APIs on, although the extreme on-your-face verbosity seems a bit unnecessary. AFAIK widely used higher level APIs have failed to really materialize. Maybe WebGPU, which is still quite low level. Game engines did get Vulkan backends quite quickly, but I wouldn't call game engines APIs as they are nowadays.
So probably you should use a game engine, even if you don't need 95% of their features. Or don't want to use languages and architectures they are married with.
There are high-level "porcelain" commands like branch and checkout.
And then there are low-level "plumbing" commands like commit-tree and update-ref.
[1] https://git-scm.com/book/en/v2/Git-Internals-Plumbing-and-Po...
Also, to some extent, Emacs. There are thousands of functions (actually, a bit less than 10k in stock Emacs without packages, and over 46k in my Emacs) performing various low-level tasks, and much fewer commands (~3k in stock Emacs, almost 12k in my config), i.e., interactive functions, often higher-level, designed for the user.
Unless you're using a trash language where even simple wrappers could buffer underrun or something.
Think about file permissions in Linux. Running ls just shows you gross file perms (current user, group, and global) but you can also grant access to other individual users, or even make a file immutable that still shows up as writable to ls. The high level api doesn’t know or care about the low level state except where it is relevant.
The high-level API needs to handle that case, if nothing better than having internal assertions that throw if it hits a case it's not designed to accommodate.
(also I'm annoyed with myself that I wrote buffer underrun in my first post instead of buffer overflow and now it's too late to edit).
Hopefully the top-level important concepts like "amount_due" will still reflect the correct numbers!
As an example, you can use chattr to make a file in Linux immutable. ls still shows that you have permission to write to the file, even though it will fail.
When people try to overthink the api and have it determine if you really can write to a file, people will try using the high level api first (chmod) and it won’t work because it has nothing to do with permissions.
KISS is really needed for high level APIs.
One level of API for implementation model.
And second level for mental model.
I’ve mainly worked in enterprise organisations or in startups transitioning into enterprise which is sort of where my expertise lies. I’ve never seen an API that wasn’t similar to the examples in this case.
I mean… I have… but they wouldn’t be labelled as high-abstraction api’s. If they needed a label it would be terrible APIs. Like sending table headers, column types in another full length array with a line for each column, and then the actual data/content in a third array. Even sending the html style that was used in what ever frontend, so that some data is represented as “some data” and other is represented as [“some data”, [“text-aligned”, “center”…],… . Yes, that is an actual example. Anyway I’ve never seen a high abstraction api and I feel like I’m missing out.
Or (as others have pointed out) the various `git` "porcelain" commands (checkout/add/commit/branch) compared to the primitive operations for dealing with various object types. Even just `git pull` as a combination of `git fetch` and `git merge`.
Or how a filesystem API of open/read/write/close/unlink is a very simple abstraction over block allocation and (in the old days) moving a disk head and waiting for the platter to spin under it in order to access the right sector. Not to mention the "directory" and "subdirectory" abstraction, instead of just having one giant table of inode numbers.
Edit: Or compare HTML with abstractions like "headings" and "paragraphs" to a raw typesetting language like troff or TeX.
Problems inevitably arise over time when there's multiple underlying systems and they have different names for the same thing, or, arguably worse, use both use a name but for different things. In this example, what if the underlying payment providers have different models? Also, what if the Federal Reserve, deprecates Input Message Accountability Data and switches to a new thing?
Maybe things are a lot simpler in the payment industry than they are in transportation or networking protocol. If I built a packet-switching product based on X.25 and later wanted to also support tcp/ip, what's the right abstraction?
For deprecations we're lucky in that the underlying systems don't change very much (the Input Message Accountability Data isn't going anywhere). But we'll run into collisions when we, for example, start issuing cards on Mastercard as well as Visa.
We have experimented with a couple of, um, abstractions, and may do so there. One rule we've stuck to, and likely would as well, is to keep the "substrate objects" un-abstracted but to introduce higher-level compositions for convenience. For example, there is no such thing as a "Card Payment" (https://increase.com/documentation/api#card-payments) - it's just a way to cluster related card authorization and settlement messages. But it's extremely useful for users and nontrivial to do the reconciliation, so we tried it. But we think it's essential that the underlying network messages (the "substrate objects") are also accessible in the API, along with all the underlying fields etc.
Unfortunately 100% of the public APIs I have worked on are in payments. I wish I had another lens!
The article clearly says it also means "no unifying similar objects", which enables the naming decision.
So rather than have a universal “goods and services not received”, it’s a 13.1 for Visa, a 4855 for MasterCard and a F30 for Amex. This matters when the boundaries are different. For example, they all split up the categories of fraud differently.
Which sounds a bit like Domain Driven Design, although the "underlying system" in this case may be a bit too implementation-centered to be considered a real business domain.
To expand on that a bit: In DDD you generally defer to the names and conceptual-models the business-domain has already created. Trying to introduce your own "improved" [0] model or terms creates friction/miscommunications, adds opportunities for integration bugs, and ignores decades or even centuries of battle-tested specialized knowledge.
I tend to agree with this. The domain concepts would be things like charge-backs and the reasons for them. The details of the codes and categories are implementation-specific. Unless, as Increase seems to be implying, their domain is the payment networks and fintech and their customers care about them the same way a kernel programmer would care about the details of memory allocators or schedulers, while most application programmers just want them to exist and work in a consistent way.
If you love Stripe (and as a designer and tech entrepreneur I do – Stripe's simplicity and front-end skill is incredible) you might look at them and copy their ability to simplify and deliver polished experiences.
But the real mastery of Stripe is that they know their customers — and the simplicity they crave.
By this article is sounds like Increase does as well and has forged a similar laser-focus on what their customers need to build terrific design guidelines for making products. Inspiring to see.
Personally I appreciate when the latter happens, but there’s an aesthetic decision there
https://thedomaindrivendesign.io/developing-the-ubiquitous-l...
It really reads like a shame response to me. People are so pathologically allergic to saying "I was wrong" or "we were wrong" that they end up pushing their metaphors around like a kid trying to rearrange their vegetables on their plate to make it look like they ate some of them.
It's also smacks of the "No defects are obvious" comment in Hoare's Turing Award speech.
Use language that your domain experts understand. If your users know about NACHA files, using other terms would mean they need to keep a mapping in their head.
On the other hand, in Stripe’s case, their users are not domain experts and so it is valuable to craft an abstraction that is understandable yet hides unnecessary detail. If you have to teach your users a language, make it as simple as possible.
The title of the concept is misleading, "No Abstractions" here doesn't literally mean "no abstractions" but instead "we use this specific set of abstractions, and not others". And the specific subset they describe is worth discussing! But it's of course a set of abstractions.
E.g.
> For example, the parameters we expose when making an ACH transfer via our API are named after fields in the Nacha specification
A specification is an abstraction.
> Similar to how we use network nomenclature, we try to model our resources after real-world events like an action taken or a message sent. This results in more of our API resources being immutable [...] and group them together under a state machine “lifecycle object”.
Immutability (in this sense) and "lifecycle objects" are abstractions.
> If, for a given API resource, the set of actions a user can take on different instances of the resource varies a lot, we tend to split it into multiple resources.
Another abstraction, just splitting at a different level than the Stripe API.
This is a set of design decisions and abstractions. Definitely not a "no abstractions" principle. I would say the most important decision they seem to have made is to generalize as little as possible -- and generalization is indeed a kind of abstraction. Maybe "Fewer Generalizations" would have been a more accurate title?
I am currently adding public API access to AI-powered text-to-SQL endpoint with RAG support and the my biggest issue is the pricing. Anybody have a ballpark figure what we could be talking about here? Pricing must account for OpenAI tokens (or perhaps letting them add their own OpenAI token), database usage and likely caching/rate limiting setup down the line.
Ex: I know that Gong costs a ton of organizations over 100k/year, and there's no way that, accounting for storage, CPUs, and all the other OpEx, that the cost comes anywhere close to the cost of compute - it's likely at least an order of magnitude greater. But because sales teams bring in so much revenue so directly, any leverage that they can buy in the form of a tool like Gong is immediately and obviously valuable.
[1]: the exception to avoiding cost-plus pricing is if you're selling a commodity. But you're not in that boat!
So the benefits are:
Audits
Network/infra
Time to market
Single API
Less code
Risk (sometimes)
The downside:
1. Slow or missing propagation of underlying features.
2. Hidden business logic.
3. Risk of changes in pricing models and so on.
4. Single point of failure.
By method, let's talk about the downsides:
1. Not the biggest risk here. But for some reason features that are new or will save you a lot of money does not propagate as fast other things.
2. Many services like payment gateways are expected to hide some aspects of the underlying services. What does this hide?
3. The big risk with something like this used to be vendor lock-in. Today it is almost always acquisitions. Is this really a product? Will it be merged and sold together with something that I don't want?
4. Obvious
Overall I think these types of services are the most useless. Abstractions that are not simplifications should mostly be avoided. I also think one needs to be extra careful if this only sits between you and other services. That is not a product in general.
This is indirection, not abstraction. Abstraction raises the semantic level.
What happens if those specifications evolve or change?
New API?
A versioned API. Which means more APIs to maintain, until they are removed.
I don't think "No Abstractions" is a good framing for this, although I would have to admit I dislike use of the term abstraction, as it implies there is a hierarchy of representations.
You might find it more valuable to state your position as "carefully scoped abstractions" to make it clear what value you add.
> without a middleman.
Hope that's clearer!
The API being implemented with JSON over HTTP isn't related to the domain of processing payments, so I don't see it as a contradiction to the article's title.
I know nothing about the lower-level details of payment networks but the mere fact that this company exists and has customers would suggest that there's a value-add.
Which to be honest is quite good, there's lots of things you can solve with an additional layer of abstraction but not having too many layers of abstraction. It's also rare to be able to identify an abstraction that correctly cuts things off at the right layer.
But then
> I don't care enough to read
Hmmmmmm.
The API only lets you set voltages on individual wires.
Google's money proto [1] has units and nanos. Any competent ad-tech system will use something similar: integer number of micro-dollars, nano-dollars, etc. You want a fair amount of precision, so just tracking whole cents isn't enough, but you want that precision to be (linearly) equally distributed across the value space so that you can make intuitive guarantees about how much error can accumulate.
[1] https://github.com/googleapis/googleapis/blob/master/google/...
¹(where you need to; otherwise, I'd use some fixed point type if my language supports it)
Having said that, half the world seems to run on Excel spreadsheets, which are full of money values, and Excel is basically all floats (with some funky precision logic to make it deterministic - would be curious to know more).
https://stackoverflow.com/questions/2815407/can-someone-conf...
Off the top of my head I can think of a few cases I would qualify as a leaky abstraction. To start with - there is a payment method abstraction and there is SetupIntent that works with it. Normal use case is tokenizing a CC. But for ACH it does something different if ever works. Same setup intent would work with debit cards, but not in Brazil because of local regulations. I don’t remember if you get a decent error code when attempt to tokenize a Brazilian debit card.
Customers making cards payments can initiate a dispute which would cost you 15 usd + payment amount if they win. This cannot happen with some other payment methods. It became important when you implement Stripe connect because you might want to set different fees for different payment methods to account for cost of disputes. The leaky abstraction part here is as soon as you start creating certain type of payment intents you also have to subscribe to Stripe webhooks for disputes.
To save on refund fees you may want to authorize payments (confirm payment intent) and capture them after a period of time. During that window you can cancel the payment and pay only authorization fee instead of paying full refund fee. This strategy works only for payment methods supporting authorization and capture semantics and having favorable commission structure. Max amount of time between confirm and capture depends on the payment method as well.
Not specific to Stripe Terminals but still. Tapping a card gives you an anonymized payment method while dipping the same card reveals some cardholder data. This is beyond Stripe control, but puzzling at first because at the API level you deal generic PaymentMethod object.
With Stripe connect what happens after the payment is defined in terms of abstract transfers between Stripe accounts. In some regions transfers works across countries while not in the others. One example is Canada-USA vs Brazil and rest of the world. From one end you have abstract transfers API to move money between Stripe accounts. From another you have to implement a number of workarounds to make transfers work in all interesting scenarios because of regional and currency conversion considerations. For example in some cases you do transfers while in other you do payment intents.
What I’m trying to say here is you have to know specifics of payment methods, underlying technologies and regions you work with. By looking at high-level API you may think it is easy to support many payment methods when in fact many of them would require very specific code.
Why do programmers always need a library between them and the API?
You do know that libraries present an API, right? Very few people program on Linux or other OSes without using libc or the OS/distribution equivalent, and for good reason. Those libraries provide a degree of compatibility across hardware systems and operating systems (and even the same OS but different versions).
Your question is about as sensible as asking "Why do programmers always need a programming language between them and the machine code?" Because it improves portability, reusability, reasonability, and on and on. Though, since you hate abstractions, maybe you do only program in machine code.
It's great that we have a succinct word to describe programmatic interfaces built on top of HTTP. It's not great that there's no longer a universally-understood word for the original more general meaning even though, as this thread demonstrates, the original meaning is still as relevant as ever.
people here are referring to some financial service on the internet, whose API is invoked over http
An article about some library might be viewed differently, i.e "X's API is better than Z's"...etc