THIS EXPLANATION
THE ROOM
CDA·127 Computing, Data & AI 6 MIN · 8 STATIONS

Letting callers ask for exactly what they want

A Socratic walk-through of letting callers ask for exactly what they want — reasoned out one step at a time, not lectured.

abcdefgh
a

The question we started with

THE QUESTION #

Why does giving callers precisely the data they request create a problem the rigid old way never had?

The complaint against fixed endpoints is easy to state. A mobile client wants a user's name and avatar; the endpoint returns forty fields including a billing address. That is over-fetching. Then the same screen also needs each of that user's last five orders, which is a second call, and the order totals need the line items, which is a third. That is under-fetching, and it is worse, because every extra round trip costs a network latency the phone cannot hide.

So the fix looks obvious: let the caller describe the shape it wants and return exactly that. GraphQL is the well-known expression of this. But it is worth pausing on the accounting before celebrating. Something was gained, and something was given up — and the thing given up was never listed as a feature of the old design, which is why it is so easy to miss until it bites.

b

Reasoning it through

REASONING #

Ask what a fixed endpoint quietly guaranteed. GET /users/42 is a named operation. The server author wrote it, knows the query plan behind it, has measured how long it takes, and can tune it. The response is identical for every caller, so it can be cached by URL anywhere along the path — browser, CDN, reverse proxy — and the cache does not need to understand the domain to do it. And the cost of the call is knowable in advance, which is what makes "100 requests per minute" a meaningful rate limit.

Every one of those properties came from the same source: the server decided the shape. Now hand shape selection to the caller and watch each one dissolve in turn.

Cost first. If a caller may select fields and traverse relationships, then two syntactically similar requests can differ in work by orders of magnitude. Asking for a user's name is nothing. Asking for a user's followers, and each of their followers, and each of their posts, is a multiplicative expansion the caller wrote and the server must honour. Counting requests no longer measures load, so a naive rate limit protects nothing. This is not a hypothetical — an unbounded nested query is a denial-of-service vector that requires no malformed input at all, just a legal one.

Then the execution shape. A flexible query is resolved field by field, and the natural implementation fetches each field's data when that field is reached. Ask for 50 orders and their customers, and the customer resolver runs 50 times, once per order: one query for the list plus N for the children. That is the N+1 problem, and it is not a bug in any one resolver — it is what per-field resolution does by default. The standard remedy is to batch: collect the keys requested within a single tick of the event loop and issue one query for all of them, which is what Facebook's DataLoader pattern was built for. But note that this is a countermeasure you must install, not a property you inherit.

Then caching. Requests now typically arrive as a POST to one endpoint with the query in the body. There is no URL that identifies the response, so URL-keyed HTTP caching has nothing to key on. Caching moves inside the application and becomes object-level — cache the entities by identity and reassemble responses from them — which works well but is a system you now own rather than infrastructure you were handed for free.

And then observability. "Which endpoint is slow?" was answerable from an access log. Now there is one endpoint. Attributing latency and cost requires instrumenting resolvers and the operations that invoke them, so the tooling has to be rebuilt at a different granularity.

What is the common thread? Every lost property was a consequence of the server knowing, ahead of time, the finite set of requests it would receive. Flexibility replaced a finite enumerated set with an open-ended language — and you cannot pre-compute anything about a set you have not enumerated.

c

The analogy

THE ANALOGY #
THE FIGURE

Compare a set menu with a kitchen that will cook anything you describe. The set menu is limiting, and you will sometimes get a side you did not want. But the kitchen has prepped for exactly those dishes, knows how long each takes, and can promise a serving time. Open the kitchen to arbitrary orders and every diner gets precisely what they asked for — while the kitchen loses the ability to predict its own evening, and one elaborate order can stall every other table.

WHERE IT BREAKS DOWN

a kitchen physically cannot start a dish it lacks the ingredients for, whereas the server has all the data and will faithfully attempt any query the schema permits, however expensive — so the protection has to be an explicit budget you impose, not a natural limit that arises on its own.

d

Clarifying the model

THE MODEL #

Three refinements.

First, the misconception this most often corrects: the difficulty is not that flexible queries are slow. Each individual query can be very fast. The difficulty is that cost has become unpredictable and caller-controlled, and the systems built around the old design — rate limits, caches, capacity plans, dashboards — all assumed predictability rather than speed.

Second, the mitigations are all versions of putting a bound back on. Depth limits reject queries nested beyond some level. Complexity analysis assigns a static cost to each field, sums it before execution, and rejects or charges against a budget — which is how a public API can meaningfully bill by work rather than by call. Persisted (allowlisted) queries go furthest: the client registers its queries ahead of time and sends an identifier at runtime, so the server is once again serving a known, finite set. Notice that the last one recovers almost every property of the fixed endpoint while keeping the developer-facing flexibility, which tells you the tradeoff is really about when the shape is fixed, not whether.

Third, none of this makes the flexible approach wrong. Where clients are numerous, varied, and outside your control — many screens, many platforms, rapid iteration — moving shape selection to the caller removes a large amount of coordination work. The honest framing is that you have exchanged a coordination cost for a governance cost, and the governance cost is the one that arrives later and surprises people.

e

A picture of it

THE PICTURE #
Letting callers ask for exactly what they want
Letting callers ask for exactly what they want One request enters on the left; band widths are the database round trips it produces when each field resolves on its own. Compare the top band -- what the caller thinks they asked for -- against the two the schema's relationships generated. Batching each wide band into a single lookup is what a loader-style fix does. {"generator":"[email protected]","source":"../Socrates/.diagram-cache/_src/letting-callers-ask-for-exactly-what-they-want.md","sourceIndex":1,"sourceLine":4,"sourceHash":"d005f627e6491820998b1de30ea3b4b82e0060cb0ad7ab5245fe141657d66df4","diagramType":"sankey","layoutVariant":"source","repairedDuplicateIds":[],"motion":"entrance-with-reduced-motion-fallback","presentation":"editorial","attempt":1,"viewBox":{"x":0,"y":0,"width":720,"height":550},"qa":{"passed":true,"findings":[]}} Onequery · 351 Orderlist · 1 Customerrows · 50 Lineitems · 300

How to readOne request enters on the left; band widths are the database round trips it produces when each field resolves on its own. Compare the top band — what the caller thinks they asked for — against the two the schema's relationships generated. Batching each wide band into a single lookup is what a loader-style fix does.

f

What became clearer

WHAT CLEARED #
WHAT CLEARED

The rigid endpoint's real product was never the JSON — it was predictability, and predictability was a side effect of the server owning the shape. Handing shape selection to the caller genuinely fixes over- and under-fetching, and it simultaneously transfers control of server cost to the party with no incentive to economise. Everything that follows — complexity budgets, batching loaders, persisted queries — is the work of buying that predictability back deliberately, once it stops being free.

g

Where to go next

ONWARD #
  • Query complexity scoring: how to assign costs to fields so a budget reflects real work.
  • Persisted queries and whether they make the flexible model converge back on the fixed one.
  • Where response caching should live once URL-keyed caching stops working.
h

Key terms

TERMS #
TermWhat it means
Over-fetching / under-fetchingreceiving more fields than needed, or needing several round trips to assemble one screen.
Resolverthe function that produces the value for a single field, invoked per field per object.
N+1 problemone query for a list followed by one query per element, produced by naive per-field resolution.
DataLoaderthe batching-and-caching pattern that collects field requests within a tick and issues a single combined fetch.
Persisted querya query registered with the server ahead of time and invoked by identifier, restoring a finite request set.

Every term the collection defines is gathered in the glossary.

Nearby on the shelf

4