GraphQL: The leakiest of abstractions — var0.xyz
-->
GraphQL: The leakiest of abstractions
2026-07-12
I fell for it. I too believed GraphQL was the future (the thing that would finally free frontend teams from the tyranny of the backlog.) I jumped on board and I loved it. Nothing to be embarrased of.
I've since changed my mind, though, and not gently. "Every abstraction leaks." That's Joel Spolsky's old law, and it holds. But GraphQL is the only abstraction I can think of that leaks in both directions at once. To understand why that matters, you have to start with what it promised.
The promise
The core pitch of GraphQL is a single sentence:
Clients can ask for exactly the data they need, in a single request, without the server defining fixed response shapes ahead of time.
Everything else flows from that. With REST, you hit GET /users/123 and the server decides the shape of the response. You get the id, the name, the email, the avatar, the preferences, the whole envelope, even if all you wanted was the name. With GraphQL, the client writes:
query {<br>user(id: 123) {<br>name
The client defines the shape. No overfetching, no underfetching, one endpoint instead of a sprawl of them, and deeply composable queries that walk relationships in a single round trip:
query {<br>user(id: 123) {<br>name<br>posts {<br>title<br>comments {<br>author { name }
Add a strongly typed, introspectable schema (to be fair, one of GraphQL's best features) and the story is compelling. But the real promise was never really technical. It was organizational.
We were coming from an industry where a distributed system was usually built by a single team with different capabilities inside it. On paper, one team. In practice, two: frontend and backend, talking about completely different things, and a dependency that ran mostly one way. The frontend depended on the backend. Need a new endpoint or a new filter? Open a ticket, negotiate with the backend, wait for it to ship, then start your own work. Delay and friction baked into the process.
GraphQL stepped into that context and said: you don't need to wait anymore. Query whatever data you need, seemingly straight from the database. Just implement this layer in between. No more dependency. No more REST. Frontend teams evolve independently from backend teams. That was the dream!
Why it leaks both ways
Software Engineering is the art of managing complexity. Complexity and dependencies don't disappear; you just decide where and when to pay for them. GraphQL didn't remove the frontend-to-backend dependency, it duplex'ed it.
A normal leaky abstraction leaks outward. Implementation details escape to the consumer: you need to know how the API works internally to use it, or an error surfaces in terminology that only makes sense to whoever wrote it. Annoying, but familiar. One direction.
GraphQL leaks in both.
Backend, inward. You set up a database. The frontend starts querying it directly through the graph. One day the database is under unusually-heavy load. What happened? You don't know, you didn't write the query that started the fire. You're responsible for the database's health but not for the code that endangers it. You own the incident without owning its cause.
Frontend, outward. You used to get a clean endpoint: known parameters, known return shape, predictable behavior. Now you can query anything. and when a query is mysteriously slow, you have to go read the resolvers to find out why. You didn't write them, but now you must understand them. The implementation has leaked straight out to you.
That's the trap. GraphQL sold "you don't need to understand the other side," but delivered "now both sides need to understand each other, at the worst possible moment." The abstraction leaks out to the frontend and back into the backend simultaneously.
The hidden tradeoff
None of this shows up on the brochure, but it's all still there:
Complexity leaves endpoint design and reappears in the execution engine.
Explicit server contracts become dynamic, arbitrary client queries.
Caching gets harder. Observability gets harder.
Query cost becomes not only unpredictable, but a genuine attack surface.
Resolver waterfalls and N+1 problems become the default failure mode.
Schema governance and federation become their own operational disciplines.
Almost everything GraphQL offers is achievable in REST: sparse fieldsets, aggregation endpoints, batch endpoints, typed contracts via OpenAPI, embedded resources. GraphQL's honest argument was never "REST can't." It was "REST gets messy at scale for complex, client-driven UIs." Fair. But the price of tidiness turned out to be a two-way leak.
The correct approach
So what's right? Well, it depends. There are only two ways to kill a dependency, either to stop invoking it altogether or to merge the two parties. Microservices make the latter feasible: once a backend is small enough that querying it is close to trivial, a group of frontend developers can own both the...