> If you’re building an abstraction-heavy API, be prepared to think hard before adding new features. If you’re building an abstraction-light API, commit to it and resist the temptation to add abstractions when it comes along.
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.
jampekka 135 days ago [-]
This. There should be a low level API to be able to do rarer more complicated cases, and a higher level simple API for common cases built on the lower-level API.
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.
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).
cpeterso 135 days ago [-]
My favorite quote about abstraction is from Edsger Dijkstra:
”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”. :)
MaxBarraclough 134 days ago [-]
That's a great quote. This topic turned up a couple of weeks ago in another thread. [0]
The excellent talk Constraints Liberate, Liberties Constrain, by Runar Bjarnason, gets at the same point, and even uses this same Dijkstra quote. [1]
Tbf Vulkan is not intended for an endprogrammer. It is a deliberately low level standardization to allow directly control GPU hardware. The high-level approach (OpenGL) failed. The endprogrammer is supposed to use a third party middleware, not Vulkan itself.
withinboredom 135 days ago [-]
Someone has to write that middleware…
f1shy 135 days ago [-]
So OpenGL is a total failure! I learned something today…
What should I use?
jampekka 133 days ago [-]
Late in the game, but OpenGL was not a failure. It was written for fixed GPU hardware pipelines, for which OpenGL was perfectly suitable. And one with which one could write application code for without middleware. And it was a lot nicer API than the contemporary Direct3D, which was forced down people's throats with the usual Microsoft monopoly tactics.
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.
wizzwizz4 135 days ago [-]
I like WebGPU: it's slightly higher-level than OpenGL, but the shader language is better. (For the actual web, use WebGL: like Wasm, WebGPU isn't actually suitable for use in websites.)
Groxx 135 days ago [-]
I'm particularly fond of this pattern when you can implement the high level API that you want outside the library, which ensures that your low level API is sufficiently flexible and means you're dogfooding your own stuff as a user. It's far too easy to get used to the internal side of a tool you're building, and forget how people actually use it.
HelloNurse 134 days ago [-]
It is also important to guarantee that the two API designs are coherent and interoperable, and this kind of strict layering is the best strategy to avoid mistakes.
paulddraper 135 days ago [-]
Git is an example of this. [1]
There are high-level "porcelain" commands like branch and checkout.
And then there are low-level "plumbing" commands like commit-tree and update-ref.
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.
That's a fantastic blog post, worthy of it's own HN submission.
yen223 135 days ago [-]
It does double your API surface area, so that's the tradeoff you'll have to consider. It can be the correct decision in a lot of cases.
Pxtl 135 days ago [-]
It doesn't double the security surface area if the abstracted API goes through the low-level API. The outer one is just chrome, and so the risks of screwing something up there is far lower.
Unless you're using a trash language where even simple wrappers could buffer underrun or something.
135 days ago [-]
lmz 135 days ago [-]
Isn't there the issue of modifying the state enough through the low-level API such that it breaks the assumptions of the high-level one?
withinboredom 135 days ago [-]
The high level API shouldn’t care about state. In other words, your “readers” should merely aggregate state and your “writers” should only care about subsets of state.
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.
eru 135 days ago [-]
Just say that you don't support mixing both APIs.
lazyasciiart 135 days ago [-]
Now you’re telling users to check the source code of any tools they use with your stuff? “We recommend using ToolOne only on content you do not also manage with ToolTwo”
eru 134 days ago [-]
See how libfuse handles it with their low level and high level APIs.
actionfromafar 135 days ago [-]
That’ll fix it. ;-)
Pxtl 134 days ago [-]
Yes. But that's the high-level API's problem. That's a problem with any abstraction really. "What if there's something in the thing we're abstracting that doesn't fit the abstraction" isn't really a problem with the "two API" approach, it's a problem with abstraction.
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).
cylemons 134 days ago [-]
You did make me search what a buffer underrun was and I think it was a good read
campbel 135 days ago [-]
Unlikely to double. The low level API exposes all capabilities, the high level API exposes a subset of those capabilities under a smaller surface. The high level API will not be as large as the low level.
Compare also exokernels, and how they delegate abstraction to libraries, not the OS.
neeleshs 134 days ago [-]
Agreed. We have a rich API to build complex pipelines, but a lot of times, users do simple mappings. So there is an API for that common use case, which has a much simpler syntax, but internally uses the rich API
_a_a_a_ 134 days ago [-]
Please email this to Microsoft
zellyn 135 days ago [-]
Problems can sneak in when you use the low-level API to do something to an object that can't be cleanly represented in the higher-level API. You need some kind of escape hatch, like a list of links to or ids of low-level details or a blob (Map<String,arbitrary-JSON> of miscellaneous data that can hold the low-level additions.
Hopefully the top-level important concepts like "amount_due" will still reflect the correct numbers!
withinboredom 135 days ago [-]
Those problems usually present themselves by people overthinking the high level api and trying to be smart.
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.
andenacitelli 135 days ago [-]
Another example of this is AWS CDK. There are a few “levels” of constructs - high level ones that are simpler and apply “presets” that are enough for 80% of users, but the core low level ones still exist if you have a nonstandard use case.
lwhi 135 days ago [-]
I like this idea a lot.
One level of API for implementation model.
And second level for mental model.
jimbob45 134 days ago [-]
That sounds like the exact philosophy the Vulkan devs took versus OpenGL.
ChrisMarshallNY 134 days ago [-]
That's pretty much what Apple does, and, I suspect, Google.
devjab 135 days ago [-]
I’m genuinely curious as to what an abstraction-rich api would look like and why it would be useful.
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.
Karellen 134 days ago [-]
Compare GTK/Qt to raw xlib/wayland/win32/cocoa primitives - including the way that those toolkits abstract away the differences.
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.
135 days ago [-]
geophph 134 days ago [-]
matplotlib seems to have implemented this approach
sameoldtune 133 days ago [-]
“Make the easy things easy and the hard things possible”
summerlight 135 days ago [-]
I like the part that explains why Increase choose a different approach. Contexts matter a lot when you design something fundamental, but people usually don't appreciate this enough.
cratermoon 135 days ago [-]
No Abstractions here really means "just use terms from the underlying system", which is a good naming principle in general.
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?
jackflintermann 135 days ago [-]
I appreciate the thorough read!
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!
advisedwang 135 days ago [-]
> No Abstractions here really means "just use terms from the underlying system"
The article clearly says it also means "no unifying similar objects", which enables the naming decision.
cratermoon 135 days ago [-]
How does that work if, for example, the example given of "Visa and Mastercard have subtly different reason codes for why a chargeback can be initiated, but Stripe combines those codes into a single enum so that their users don’t need to consider the two networks separately.". Unfortunately, the article doesn't explain how Increase handles that overlap. Presumably, as the article states, their customers are the sort that do care about Visa reason codes vs Mastercard reason codes, so what's the design of a "no abstraction" API in that case?
travisjungroth 135 days ago [-]
I’m just reasoning from my limited experience and the article. Stripe unifies those reasons codes. Increase doesn’t. It might be that the Chargeback has the processor and chargeback code as attributes.
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.
Terr_ 135 days ago [-]
> No Abstractions here really means "just use terms from the underlying system"
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.
> the "underlying system" in this case may be a bit too implementation-centered to be considered a real business domain.
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.
spandrew 135 days ago [-]
Love the article.
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.
Yeah I do think you can see in Stripes API places where there are differing tensions between “let’s make this potentially universal” and “let’s accept that this stuff is going to probably only apply for one payment method in one market”.
Personally I appreciate when the latter happens, but there’s an aesthetic decision there
cpeterso 135 days ago [-]
This is similar to Domain-Driven Domain's "Ubiquitous Language" design pattern, making your implementation use the same real-world terminology used domain experts.
I was introduced to this concept a good while before DDD came along, when someone opined that if the nouns and verbs in your code don't match the problem domain that's an impedance mismatch and it's going to get you into trouble some day.
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.
theptip 135 days ago [-]
This is a great example of the concept “ubiquitous language” from Domain Driven Design.
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.
Nevermark 135 days ago [-]
Or to put it another way, they are domain experts in the kinds of transactions they want to perform, not how transactions are implemented in the financial system.
jackflintermann 135 days ago [-]
Author here - this has been a useful mindset for us internally but I'm curious if it resonates externally. I'd love your feedback!
west0n 135 days ago [-]
If we didn't have abstractions like POSIX, applications would need to write an adaptor for every supported file system.
the_af 134 days ago [-]
Interesting.
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?
l5870uoo9y 135 days ago [-]
> Monthly fees for users building on Increase vary by use case.
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.
chaos_emergent 135 days ago [-]
Foundationally, pricing should be based on value, not cost[1] so you should think about what the value is to your customer and go from there.
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!
AtNightWeCode 133 days ago [-]
But the entire API is an abstraction...
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.
MaxBarraclough 134 days ago [-]
> Visa and Mastercard have subtly different reason codes for why a chargeback can be initiated, but Stripe combines those codes into a single enum so that their users don’t need to consider the two networks separately.
This is indirection, not abstraction. Abstraction raises the semantic level.
lwhi 135 days ago [-]
So they say parts of the API structure are based 1-1 on externally controlled specifications.
What happens if those specifications evolve or change?
A versioned API. Which means more APIs to maintain, until they are removed.
lwhi 134 days ago [-]
More complexity is bad imo.
Supermancho 133 days ago [-]
I agree.
tegling 135 days ago [-]
One tricky thing to model neatly in payment APIs is that payment schemes indicate the roles of payer and payee in payment returns in different ways. E.g. for one particular scheme the payer and payee may be kept in the same position as in the initial payment (creditor of payment return is actually the one sending funds) whereas in another one they are switched (creditor is the one receiving funds in the return). I'd be curious to see how they are handling this case as it can be a real head-scratcher.
Splizard 134 days ago [-]
I think the point the author is trying to express here, is that it can be a useful in API representation, to couple the design with other representations designed by other parties.
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.
chowells 135 days ago [-]
If there's no abstraction, what's your value-add? I don't care enough to read your marketing BS to see where you claim to be special, but... If your API is doing the exact same things as an underlying service is doing, you're just a middleman extracting rents.
You might find it more valuable to state your position as "carefully scoped abstractions" to make it clear what value you add.
koreth1 135 days ago [-]
Based on my previous experience on payment systems, there's a surprising amount of value in not having to maintain direct business relationships with the underlying payment providers. It is much, much easier to work with a company like Stripe than to work directly with Visa and MasterCard and the ACH network, and heaven help you if you're a small company that needs to do automated cross-border payments to a wide range of countries without a middleman. You'll probably also get much better support from a tech-focused company when an API starts freaking out.
jackflintermann 135 days ago [-]
Yes, exactly, the important thing to us is that our users don't need to build an additional mental model between us and the networks we sit atop. If you know the network, we want you to be able to intuit how our API works. There's a very real difference (arguably the fundamental value-add of our company) in the transport layer, though. The actual mechanics of integrating with, say, FedACH, are a bit long to get into here (we get into it a bit here if it's of interest: https://increase.com/documentation/fedach) but suffice to say it doesn't have a REST API.
wodenokoto 135 days ago [-]
But it must have _some sort_ of API. Since your rest API is modelled on their API it made me really curious about how you communicate with those networks.
koreth1 135 days ago [-]
That's an excellent point too. Some payment systems have abysmal technology. The product I worked on was focused on international payments and in a couple cases, the "API" was literally, "Upload a CSV file via FTP, and at some later point, another CSV file might appear on the FTP server with some of the payment results, but if it doesn't, call us because we probably just forgot to upload it."
nijave 135 days ago [-]
Batch jobs and (S)FTP. In a bit of a weird twist, back when I worked at Chase, they were innovating on the ancient technology but it was things like "better batch job management/orchestration" and SFTP proxy to route between different servers and centralize key management
jrochkind1 135 days ago [-]
I'm curious to learn more about what your customers look like, what sorts of businesses and activities they are in. Where stripe's customers are working on products unrelated to payments, yours are working on products related to payments? I'm having trouble conceiving of examples.
fendy3002 135 days ago [-]
Idk how this one works, but credit card processors need license. If you can use credit card service without requiring that license then it'll be the best additional value.
exe34 135 days ago [-]
I thought I understood everything you said, but isn't Stripe a middleperson here?
> without a middleman.
koreth1 135 days ago [-]
Right, Stripe is a middleman and part of the value they're giving you is that you don't have to work directly with the underlying payment companies. If you had to support the same range of payment options without a middleman, you'd need to have business relationships with a bunch of payment companies, which would be a lot more difficult and time-consuming.
Hope that's clearer!
exe34 135 days ago [-]
Makes sense thanks!
OJFord 135 days ago [-]
Yes, GP's point is 'good luck to you doing that yourself, without a middleman [such as Stripe]'.
conroy 135 days ago [-]
In this case, a HTTP API is the abstraction. Integrating with ACH and other payment rails requires a lengthy integration process. Sometime you have to send binary files using FTP!
jiggawatts 135 days ago [-]
The article says “no abstractions”, but HTTP is often exactly that: an abstraction over lower-level protocols.
gr4vityWall 134 days ago [-]
My interpretation is that they meant domain-level abstractions. So, their API endpoints won't try to hide details about the underlying payment methods through abstractions, because that works best for those users.
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.
jackflintermann 135 days ago [-]
I guess the phrase "no abstractions" is specifically valuable to us when designing our REST API resources - our whole stack is certainly an abstraction of sorts, but we don't want to add yet another abstraction in that specific layer.
gr4vityWall 134 days ago [-]
Just wanted to say that I appreciated the article :)
Using well-designed APIs is great, and seeing people putting a lot of thought on it, with the intent of improving dev experience, is very refreshing. I've dealt a lot of technically impressive Free Software projects that didn't focus on this as much, and as a result, using their libraries was harder.
jackflintermann 134 days ago [-]
Thank you!
chaos_emergent 135 days ago [-]
perhaps what you're thinking of is "equal entropy abstractions" - HTTP is just a way of standardizing logic, but the complexity of the shape and behavior of the API remains.
arrowsmith 135 days ago [-]
Presumably the value comes from providing a single unified platform that means you don't have to integrate with every underlying service separately.
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.
contravariant 135 days ago [-]
There is abstraction there's just not an additional layer of abstractions on top of it.
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.
alex_lav 135 days ago [-]
> If there's no abstraction, what's your value-add?
But then
> I don't care enough to read
Hmmmmmm.
dheera 135 days ago [-]
Java engineers need to see this. Goddamn Proxies, Factories, and Beans. Fragments, Surfaces, Runnables.
RobotToaster 135 days ago [-]
No abstractions? So their API lets me control the individual registers on their CPU then?
dkjaudyeqooe 135 days ago [-]
No, sorry, registers are an abstraction.
The API only lets you set voltages on individual wires.
layer8 134 days ago [-]
Do you also have to provide the clock signal for processing? Might become expensive in terms of API calls.
dkjaudyeqooe 134 days ago [-]
I had to think about it for a sec, but clock signals are indeed an abstraction so you have to provide them.
bvrmn 135 days ago [-]
Also it greatly helps to not overuse nouns and try to forcefully model verbs with resource entities.
lolpanda 135 days ago [-]
for any APIs related to money, should the currency be in strings as opposed to in floats? This will prevent accidental float arithmetic in the code. I always find it tricky to work with currency in javascript.
benhoyt 135 days ago [-]
From their docs [1] it looks like they do everything using integers: the amounts are integers in the "minor unit" of currency, for example cents if the currency is dollars. So 1000 means $10.00. In languages like JavaScript where everything is a float64, you can still accurately represent integers up to 2^53, which would be $90 trillion.
This isn't sufficient to represent prices which often include fractional amounts of cents in non-retail scenarios. Think of AWS server prices per hour.
But those are decimal values, not integers. I didn't mean that using cents as a unit was insufficient, I meant that using integers was.
135 days ago [-]
freedomben 135 days ago [-]
Yes, never use floats for currency. I typically use integers and for USD for example, measure in "cents" rather than dollar. I try to avoid the fallacy of appeal to authority, but this is what Stripe does. You can also use the Decimal type in javascript and convert to/from strings to cross API boundaries.
GeneralMayhem 135 days ago [-]
Ideally integers, but at a large multiplier.
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.
I will be the contrarian: JSON numbers are not floating point values, they are strings of characters matching the format "-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?". You can choose to parse them however you want, and parsing libraries should provide a mechanism to decode to an arbitrary-precision value.
lolpanda 135 days ago [-]
Good point. it's not a problem with JSON. It's just that most of the JSON libraries by default parse numbers into floats.
int_19h 135 days ago [-]
Regardless of what the libraries should be doing, there is the reality of what they are doing.
tadfisher 134 days ago [-]
Luckily, there are not many broken JSON parsers out there, as this is a well-known issue. Just about the only parser that remains broken is Javascript's JSON.parse(), because it doesn't allow for any options to control the parser, which is why we keep having this discussion.
koreth1 135 days ago [-]
By way of example: when I worked on payment code in Java, we accepted numeric JSON values in request payloads but they were deserialized into "BigDecimal" fields in our payload classes, not "float" or "double".
meekaaku 135 days ago [-]
Yes, but say in javascript if you do a JSON.parse(), it will give you a double float right?
tadfisher 134 days ago [-]
Yes, because JSON.parse() treats the input as if it were a JS object literal; that is, JSON.parse(JSON.stringify(someObject)) will be idempotent. Usually parser libraries let you construct a parser with options, so you'd make a "let json = JSON({parseNumbersAsBignums: true})" and use that everywhere, but JS doesn't have that built in.
135 days ago [-]
trevor-e 135 days ago [-]
I've always seen currencies multiplied by 100 to remove the need for floating point.
kadoban 135 days ago [-]
If you use a higher constant, 10000 or 1000000 or something, you give yourself a good amount of more fleixibility.
cateye 135 days ago [-]
Some currencies use more than 2 decimal places. For instance, the currencies of Algeria, Bahrain, Iraq, Jordan, Kuwait, Libya, and Tunisia are subdivided into 3 decimals.
akavi 135 days ago [-]
That's not quite a sufficient rule. Eg, 1 Bahraini Dinar is 1_000 Bahraini Fils.
nijave 135 days ago [-]
Yeah, this seems like a common pattern. Not sure about currency with arbitrary place values though (like Bitcoin)
deathanatos 135 days ago [-]
I'm not sure what you mean by "arbitrary place values" with Bitcoin; if you are implying it's infinitely divisible, it isn't. You'd do the same trick with Bitcoin: represent it as an integer¹. The value 1 is 1 sat, or 0.00000001 BTC.
¹(where you need to; otherwise, I'd use some fixed point type if my language supports it)
cratermoon 135 days ago [-]
neither. Use rational or some other better type.
endofreach 135 days ago [-]
You should never use floats for dinero. And it has nothing to do with JS, though i find it funny that you mention JS.
xxgreg 135 days ago [-]
Don't use floats if you're trying to represent an exact value, i.e. someones bank account. But in financial modelling you're generally dealing in probabilistic "expected values", it's common and fine to use floats.
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).
I think this is better than Stripe’s abstract everything approach even for people who are not into payments. Stripe has built a very leaky abstraction.
adelineJoOs 135 days ago [-]
How is the leakage noticeable?
kikimora 135 days ago [-]
I’m not saying Stripe API is bad. But there are limits to how much differences you can hide behind a generic API while keeping it consistent.
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.
danecjensen 135 days ago [-]
Strong work Jack!
freguencey 135 days ago [-]
[dead]
shironandonon_ 135 days ago [-]
TLDR: we are special and created our own standard.
ngrilly 135 days ago [-]
They are saying the opposite: we follow existing standards as much as possible.
andrewstuart 135 days ago [-]
I hate abstractions. Program the thing as it is intended.
Why do programmers always need a library between them and the API?
Jtsummers 135 days ago [-]
> 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.
koreth1 135 days ago [-]
I kind of hate the fact that the term "API" has lost its generality in the minds of a huge number of practitioners, and people now assume it refers to a set of network (usually HTTP) request and response formats.
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.
compootr 135 days ago [-]
I think context has to be taken into account
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
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.
[0] https://xkcd.com/793/
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.
[1] https://increase.com/documentation/api#transactions
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