skip to content

An HTTP API has one endpoint per database table, and rendering a single screen takes eleven calls. A colleague proposes one large endpoint that returns everything the screen needs. Evaluate both extremes and describe how you would actually choose the granularity of resources.

level: seniorimportance: should knowfreq 50%

answer

  1. chatty = RTT-bound + client-side joins + no consistency
  2. god-endpoint = UI coupling + uncacheable + max latency, product uptime
  3. split on permission / volatility / boundedness / concurrency
  4. merge on atomicity — no cross-resource transaction
  5. aggregate = domain concept, not screen name; BFF isolates UI coupling

basics

~20 s

Fine-grained resources are chatty and push joins onto clients; one screen-shaped mega-resource couples the API to a UI, ruins caching and invalidation, and grows without bound. Choose by interaction: resources sized to how clients actually use data, with composition for genuinely joint reads and splits where permission, volatility or size differ.

solid answer

~60 s

**Too fine** (table-per-endpoint): eleven round trips per screen, latency dominated by RTT, every client re-implements the same joins, and consistency across the eleven responses is nobody's job. It also publishes your schema as your contract. **Too coarse** (one screen endpoint): the API now encodes a UI layout, so every design change is an API change; the response mixes fields with different permissions and change rates, so it is effectively uncacheable; partial updates become ambiguous; and one slow dependency degrades everything. **How I choose:** size resources by *interaction*, not by table or by screen. 1. List the real client interactions and what each needs together. 2. Split where permission, volatility, or unboundedness differ — those are cache and authorization boundaries. 3. Keep together anything that must change atomically; never make a client responsible for a server invariant. 4. Where a genuinely joint read exists, offer composition — an aggregate resource for a stable, named concept (`/checkouts/{id}`), or opt-in expansion on the parent, rather than a screen-named endpoint. 5. Measure: round trips per interaction, p99, cache hit rate. Move the boundary where the numbers say so.

go deeper

for a junior

Name both problems — too many calls versus one endpoint that returns everything — and say resources should follow the domain, not tables or screens.

for a middle

Give the split and merge forces concretely, and mention expansion or an aggregate resource as the middle path.

for a senior

Lead with caching, invalidation, availability composition and atomicity, and propose a BFF as the place UI coupling belongs.

for a principal

Set the organizational rule: the core API is domain-shaped and multi-consumer, composition layers are owned by client teams, and granularity is revisited against measured round trips, hit rates and latency.

## The two failure modes **Chatty (over-fine).** One endpoint per table is easy to generate and terrible to consume. Symptoms: N+1 fetch patterns from the client; latency dominated by round trips rather than server time (on a 100 ms mobile RTT, eleven sequential calls is over a second before any work happens); every client re-implementing the same joins, in different languages, with different bugs; and no consistency guarantee — the eleven responses are eleven point-in-time snapshots, so the UI can show a total that matches none of the parts. It also makes your storage schema the public contract, so normalisation changes become breaking API changes. **God-endpoint (over-coarse).** One `/screens/dashboard` fixes the round trips and creates worse problems. The API is now coupled to a UI: a redesign is an API change, and a second client (mobile, partner, internal) either gets a payload shaped for someone else's screen or gets its own endpoint, so you grow one per screen per client. Caching collapses because the response mixes a stable profile with a live balance — a single `Cache-Control` and ETag must satisfy the most volatile member, so effectively nothing is cached. Invalidation is intractable: any change to any contributing entity invalidates the whole document. Writes are ambiguous — what does `PUT /screens/dashboard` mean? And availability degrades: the response needs every dependency, so its uptime is the product of theirs and its latency is the max. ## The right axis: interaction, not storage or layout Resource granularity should follow **how clients use the data**, which is usually neither one-per-table nor one-per-screen. Practical procedure: **1. Enumerate interactions.** "Show an order summary", "change a shipping address", "check balance". For each, list what must be fetched or changed together, and how often it happens. **2. Apply split forces.** Split when parts differ in *permission* (elevated fields), *volatility* (cache lifetime), *size or boundedness* (an ever-growing list must not live inside a parent representation), or *concurrency* (one ETag over an aggregate makes unrelated edits collide with 412s). **3. Apply merge forces.** Merge when parts are always used together, share a lifecycle, or must change atomically. This last is decisive: HTTP has no cross-resource transaction, so if updating A without B leaves the system invalid, they are one resource. **4. Compose without coupling to the UI.** When a genuinely joint read exists, you have options that do not name a screen: - **Aggregate resources for real domain concepts.** `/checkouts/{id}` is a legitimate concept in the business, not a layout. It composes cart, pricing and shipping because the *domain* treats them as one thing. - **Opt-in expansion.** Keep resources separate but let a caller inline named relations, so the default stays cacheable and the composed form is available on request. - **A separate composition layer.** A backend-for-frontend can own screen-shaped endpoints while the core API stays domain-shaped. This is the honest place for UI coupling: it isolates it in a component owned by the UI team and redeployed with the UI. **5. Measure and move.** Instrument round trips per interaction, p99 latency, payload bytes, cache hit rate, and how often clients fetch a resource only to use two fields. Granularity is not a one-time decision; it is a boundary you adjust with evidence. ## Rules of thumb - If two clients would want different subsets of one endpoint's response, it is probably too coarse. - If one client always calls two endpoints together and joins them, they are probably too fine — but check whether the join is stable across clients before merging. - Never let a resource contain an unbounded collection inline; link to it. - Never let a resource contain fields the same caller cannot all read; that forces per-field redaction and makes the shape unpredictable. - Prefer the mistake of slightly-too-fine plus composition over slightly-too-coarse: it is far easier to add an aggregate over stable fine resources than to decompose a coarse one that clients depend on field by field. ## The answer to give Both extremes are wrong for the same underlying reason: they take their shape from something other than the domain — one from storage, one from a screen. Model the domain, split on permission/volatility/boundedness, merge on atomicity, and add composition deliberately where measurement shows round trips hurt.

  • Where should screen-shaped composition live if not in the core API?
    In a backend-for-frontend owned and deployed by the client team. It can be as UI-shaped as it likes because it changes with the UI, while the core API stays domain-shaped and serves multiple consumers. The cost is another service to operate, so it is worth it once you have more than one substantially different client.
  • Why is a coarse aggregate endpoint hard to cache well?
    Its cache lifetime is bounded by its most volatile member, so a document containing both a stable profile and a live balance can only be cached as long as the balance is valid — effectively not at all. Invalidation is equally bad: a change to any contributing entity invalidates the whole document, so hit rates stay low even with an ETag.
  • You must pick a starting granularity before you have usage data. What do you do?
    Start slightly fine, following domain concepts rather than tables, and add composition where measurement shows round trips hurt. Adding an aggregate over stable resources is straightforward; decomposing a coarse resource that clients already depend on field by field is a breaking change. Instrument round trips per interaction from day one so the evidence exists when you need it.

Selling a car as 30,000 individual parts, or only as a fully-configured showroom package — the useful catalogue sits between: engine, wheels, seats, assembled the way people actually buy them.

saying these in an interview costs you the question

  • Naming endpoints after UI screens in the core API
  • Assuming fewer endpoints is automatically better because it means fewer round trips
  • Ignoring that a coarse response's cache lifetime is set by its most volatile field
  • Splitting resources that must be updated atomically and expecting the client to coordinate
  • Treating granularity as a permanent decision rather than a measured, movable boundary

context