SwadhyAI Notes

All System Design notes

System Design APIs

GraphQL

Imagine opening Instagram and waiting for one feed card: post text, author avatar, like count, and the top three comments. With classic (one URL per resource, fetched with HTTP verbs), that card can take a waterfall of requests, and each response may ship fields the phone never shows.

was born for that pain. The client describes one shaped tree of fields and gets it back in a single round trip. The trade-off is that the server now does the fan-out work, so you must watch query cost, (one extra database call per child row), and caching that no longer keys cleanly on a URL.

Feed card journey

1

Client writes a query

The card component lists exactly the fields it renders, and nothing more.

2

Unknown or disallowed fields are rejected before any resolver does expensive work.

3

Each requested field loads from a service or database through its own resolver function.

4

Batches nested author lookups into one query so N+1 fan-out does not explode.

Try it: GraphQL resolver fan-out versus batching

Execute the same nested feed query under naive per-field fetching and under request-scoped batching. Count backend calls instead of guessing from the GraphQL shape alone.

P1P2P3P4P5P6P7P8P9P10→A1A2A3A4A5A6A7A8A9A10
Each amber author box is a separate backend round trip in the naive model. Batching preserves the GraphQL shape while shrinking fan-out.
Queries executed
0
Backend calls (cumulative)
0
Posts per query
10

Run a feed query that loads ten posts and each post's author. Naive resolvers issue one backend call per post; batched loading collapses author lookups.

What actually happens in one GraphQL request

The feed card journey above compresses four steps; here they are slowed down. The client posts (usually) to a single endpoint with a query document. That document is not "give me the user resource". It is "give me name, avatar, and the first three comments with author names." The server checks the document against its (the typed contract of every field a client may ask for), runs a for each requested field, and returns a response tree that matches the query tree exactly.

That one mechanism fixes two REST problems at once. is when one response is not enough, so the client chains follow-up calls and pays a round trip for each. is when a fixed response carries fields the screen never shows, so bandwidth, parse time, and memory are wasted. Nesting the fields you want in one query removes the extra round trips; listing only those fields removes the wasted bytes. The server still has to do the work, though: nested resolvers can fan out into many database calls unless you batch them with (a per-request batching and caching helper) or with joins at the service layer.

Two comparisons frame the rest of the page. Compared with , you gain shape flexibility and lose easy URL-based HTTP caching, because queries often use POST to one URL. Read queries can also use GET when supported, which enables HTTP caching with an appropriate cache key. Compared with (typed binary RPC between services you control), you stay JSON and human-friendly at the product edge, but you are not optimizing for internal throughput.

Why GraphQL was born: one feed card with REST

The prose above names the two problems; this section shows the numbers that made them hurt. In 2012, a social feed app's mobile client was slow and data-hungry because forced too many requests for too much data. To render one single post card, here is what REST required, and what the same card looks like as a single query.

❌ REST: 7 sequential/parallel requests

javascript
// Rendering ONE post card needs:
GET /api/feed              → post IDs
GET /api/posts/456         → post content
GET /api/users/123         → author name + avatar
GET /api/posts/456/likes   → like count
GET /api/posts/456/comments?limit=3  → top 3 comments
GET /api/users/789         → comment author 1
GET /api/users/101         → comment author 2

// 7 round trips! On 3G mobile = 7 × 200ms = 1.4 seconds
// AND each returns ALL fields (over-fetching)

✅ GraphQL: 1 request, exact data

graphql
query NewsFeed {
  feed(first: 10) {
    posts {
      id
      text
      createdAt
      author { name, avatarUrl }
      likeCount
      comments(first: 3) {
        text
        author { name }
      }
    }
  }
}

// 1 round trip! On 3G = 200ms
// Returns ONLY the fields listed above

: the chatty waterfall

The seven requests above are under-fetching in action: one endpoint does not give you enough, so you chain more, and on a high-RTT (round-trip time) mobile network each link in the chain is paid in full before the UI can render.

Client→GET /users→GET /posts ×N→GET /authors ×N

Latency ≈ (1 + N + N) × RTT before the UI is complete

javascript
// You need: user name + their posts + follower count
// REST gives you:

GET /users/123
→ { id, name, email, bio, phone, address, ... }
// ✅ Got name, but no posts or followers

GET /users/123/posts
→ [{ id, title, body, tags, ... }, ...]
// ✅ Got posts, but still no followers

GET /users/123/followers/count
→ { count: 1420 }
// ✅ Finally got everything!

// 3 round trips. 3× latency on slow networks.

The cost: on a 100ms RTT mobile connection, three sequential requests mean 300ms minimum before the UI can render anything.

: wasted bytes

The other half of the pain is that each of those fixed REST responses returns every field of the resource. A comment card only wanted the author's name, but got the entire user object.

Needed
~20 B
REST
~8 KB

Same endpoint shape for every client → over-fetch by default

javascript
// You only need: user's name for a comment card
GET /users/123
→ {
    id: 123,
    name: "Alice",           // ✅ needed
    email: "a@b.com",        // ❌ wasted
    phone: "+1-555-0123",    // ❌ wasted
    address: { ... },        // ❌ wasted (500 bytes)
    preferences: { ... },    // ❌ wasted (1KB)
    paymentMethods: [ ... ], // ❌ wasted (2KB)
    loginHistory: [ ... ],   // ❌ wasted (5KB)
  }
// You needed 20 bytes, got 8KB. 400× waste.
// ×1000 users on news feed = 8MB wasted data.

The cost: wasted bandwidth, slower parsing, and higher memory on the device, especially painful on metered 3G/4G connections.

How GraphQL solves both problems at once

1. Exact fields, no over-fetching

The client lists only what the component uses, and the server returns nothing more.

2. One round trip, no under-fetching

Related data is nested in a single query, so there is no waterfall of chained calls.

3. Shape matches the UI

Query shape equals response shape equals component tree, so no mapping layer is needed.

Complete example: a user profile page

Putting both halves together on a second screen, here is the full REST versus GraphQL comparison for a profile page.

REST: 4 requests, ~12KB response

http
// Request 1
GET /users/123     → 8KB (all fields)
// Request 2
GET /users/123/posts?limit=5  → 3KB
// Request 3
GET /users/123/followers/count → 50B
// Request 4
GET /users/123/following/count → 50B

// Total: 4 RTTs, ~12KB transferred

GraphQL: 1 request, ~800B response

graphql
query UserProfile {
  user(id: "123") {
    name
    avatarUrl
    bio
    posts(first: 5) {
      title
      createdAt
    }
    followerCount
    followingCount
  }
}
// Total: 1 RTT, ~800B transferred

93% less data transferred and 75% fewer round trips. On mobile, that is the difference between instant and a spinner.

How to choose GraphQL

Those numbers explain the appeal, but they do not by themselves justify for your system. It is as much an organizational choice as a technical one, and three questions decide it.

1. Do clients need different shapes from the same graph? The iOS team needs 5 fields, Android needs 8, web needs 12. With you either build custom endpoints per client (endpoint explosion, often as a BFF, a backend-for-frontend layer per screen) or return everything and over-fetch. If that divergence keeps exploding BFFs or bloating REST DTOs (data transfer objects), GraphQL is a strong fit: each team queries exactly what it needs from the same endpoint.

2. Are frontend requirements still changing weekly? With REST, a redesigned card means the frontend files a ticket, the backend adds an endpoint, and everyone waits for a deploy. With GraphQL the frontend changes the query and ships immediately. That works because the backend owns the (what is possible) while the frontend owns the query (what is needed), so the schema is the contract and both teams work in parallel without meetings about which fields to include.

3. Are the requirements already fixed and simple? A known set of screens over a handful of CRUD resources rarely justifies GraphQL overhead; REST with a few well-designed endpoints, or a thin BFF, is usually enough. Saying "we'll use GraphQL to be modern" without naming under-fetching, over-fetching, or client divergence is a weak signal.

Simple rule of thumb

Name the pain first, then the protocol. Use GraphQL when multiple clients and nested relationships make fixed REST shapes painful, and when the problem explicitly values query flexibility or rapid frontend iteration without backend redeploys. The three-way REST, , and GraphQL comparison further down covers when the other two win.

Signals to look for

Strong signals for GraphQL

  • Multiple client types (web, iOS, Android) that each need a different slice of data.
  • Many teams or separate backend and frontend orgs that want to ship independently.
  • Complex nested data such as social graphs or e-commerce catalogs with deep relationships.
  • Frequent UI changes that should ship without waiting on a backend deploy.
  • Mobile-first products on slow networks, where every wasted byte delays the first render.
  • Microservices that need one unified API through (covered below).
  • News feed or aggregation screens that stitch data from several backend services.

Strong signals against GraphQL

  • Simple CRUD with 5–10 resources, where a few REST endpoints are easier to run.
  • Tiny team or MVP, where the operational overhead is not worth it yet.
  • Heavy file uploads, because binary data is awkward in the GraphQL spec.
  • Cache-heavy public API, where HTTP GET caching is far simpler with REST.
  • Single client type, so there are no fetching shape differences to serve.
  • Public API with unknown clients, which is harder to rate limit fairly.
  • Real-time high-volume streaming between services, where gRPC is usually the better fit.
  • Team lacks GraphQL operational expertise for cost limits, caching, and field-level authorization.

Queries, mutations, and subscriptions

The schema defines available fields and types; a query selects from that contract. A client cannot request a new business capability merely by naming a field that the backend has not implemented. Mutations identify operations that may change state, but GraphQL itself does not make a multi-field mutation into a database transaction. The resolver’s storage logic must provide any required atomicity.

Once you have decided GraphQL fits, the next thing to learn is its three operation types. Reads, writes, and push-style updates each use a distinct operation, and all three are typed by the same .

Query (Read)

graphql
query {
  user(id: "123") {
    name
    email
    posts(first: 5) {
      title
      likes
    }
  }
}

One read request replaces three or more REST calls for the same screen.

Mutation (Write)

graphql
mutation {
  createPost(input: {
    title: "Hello World"
    body: "Content here"
  }) {
    id
    createdAt
  }
}

The write path, like POST, PUT, or DELETE in REST, and it returns the fields you ask for.

Subscription (Real-time)

graphql
subscription {
  newMessage(chatId: "abc") {
    id
    text
    sender { name }
    sentAt
  }
}

Live updates pushed over a WebSocket connection whenever the selected event fires.

Resolvers, server-side fan-out, and the N+1 problem

Every field in those operations is filled by a (the function that loads one field's data), and that is where the server-side cost hides. The client saved its round trips; the gateway now makes them instead.

📰 Real pattern: GraphQL as the news feed aggregation layer

Not always, but for news feed type applications, is often the single aggregation endpoint that stitches data from multiple backend services:

📱 Mobile App→
GraphQL Gateway
Single endpoint
→
User ServicePost ServiceComment ServiceLike Service
graphql
# News feed query: one request aggregates from 4 services
query NewsFeed($userId: ID!, $limit: Int!) {
  feed(userId: $userId, first: $limit) {
    posts {
      id
      text                          # → Post Service
      author {
        name, avatarUrl             # → User Service
      }
      likeCount                     # → Like Service
      topComments(first: 3) {
        text                        # → Comment Service
        author { name }             # → User Service (batched)
      }
    }
    pageInfo { hasNextPage, endCursor }
  }
}

Why this works: the gateway orchestrates the fan-out to backend services server-side, over a fast internal network, instead of the mobile client making four or more requests over slow cellular. batches the user lookups across comments, and the client gets one shaped response matching its UI.

The problem and DataLoader

That server-side fan-out is also the biggest performance pitfall in GraphQL. Nested fields trigger one database query per child row unless something batches them: without mitigation, a query for 100 users with their posts makes 1 query for users plus 100 queries for posts, 101 database queries in total.

Without DataLoader (N+1)

sql
SELECT * FROM users LIMIT 100;  -- 1 query
SELECT * FROM posts WHERE user_id=1;  -- query 2
SELECT * FROM posts WHERE user_id=2;  -- query 3
... (98 more)
-- Total: 101 queries!

With DataLoader (batching)

sql
SELECT * FROM users LIMIT 100;  -- 1 query
SELECT * FROM posts WHERE user_id IN (1, 2, 3, ..., 100);  -- 1 query
-- Total: 2 queries!

The DataLoader pattern

  • Collect every ID requested during one event-loop tick instead of querying immediately.
  • Batch those IDs into a single database query using WHERE id IN (...).
  • Cache results for the lifetime of the request, so a repeated author loads once.
  • Keep that cache per request, not global, so stale data never leaks between users.

A large social app invented DataLoader for this exact problem.

Worked example: N+1 cost on a feed

Batching collects author identifiers requested during one execution window and loads them together. The batch result must map back to the original identifiers, including duplicates and missing records, so each resolver receives the correct author. Scope a loader to the request or authorization context: a global memoized loader can return data fetched under another user’s permissions. Batching reduces query count; indexes, maximum list sizes, and a bounded database connection pool still determine the cost of each batch.

For a feed of 50 items resolving their authors, an unbatched implementation may issue one feed query plus 50 author queries. If those calls execute sequentially and each waits 5 ms, network waiting alone is about 255 ms. Concurrent calls overlap that time but still create 51 queries and can exhaust the connection pool. Batching the authors can reduce this to two queries; the real latency then depends on the database work and scheduling, not only the round-trip count.

GraphQL execution cost

User service

Feed service

Media service

Payload and bandwidth budget

Header overhead

Assume each request carries 2 KB of headers and the service receives 50,000 requests per second. Multiplying bytes per request by requests per second gives 100 MB/s of request headers alone, using decimal units. This excludes response bodies and lower-level overhead. Reducing a cookie helps repeatedly because the browser may attach it to every matching request.

Field trimming

8KB response trimmed to 800B saves 7.2KB. Across 1B reads, that is about 7.2TB less transfer.

Compression

100KB JSON compressed to 20KB saves 80KB. Across 10M responses, that is about 800GB less egress.

Fan-out

One 600B event to 10K subscribers becomes 6MB of outbound payload, before protocol and encryption overhead.

Keeping query cost bounded

Depth alone is insufficient. A query only three levels deep can request 10,000 posts with 1,000 comments each, creating ten million child results. Combine per-field cardinality limits with an estimated total cost and a runtime budget. Charge expensive queries according to their work rather than counting every HTTP request as equal. Rejecting excessive work before execution protects the database; cancellation and concurrency limits handle estimates that prove too optimistic.

Batching fixes accidental fan-out; it does nothing against a query that is deliberately or clumsily enormous. Because clients write their own queries, a malicious or careless one can fan out exponentially, so the server must guard cost before execution with depth limits, complexity scores, timeouts, rate limits, and (pre-approved query shapes, covered in the next section).

Malicious deeply nested query (denial of service):

graphql
query {
  user {
    friends {
      friends {
        friends {
          posts {
            comments {
              author {
                friends { ... }
              }
            }
          }
        }
      }
    }
  }
}

Five guards, applied before a single resolver runs

Max Depth Limit

Reject any query nested deeper than 10 levels (depth ≤ 10) before execution starts.

Complexity Analysis

Assign cost per field: Scalar=1, Object=5, List=5×estimated_size. Max total: 1000/query.

Query Timeout

Kill any query still running after 10 seconds, so one caller cannot hold connections indefinitely.

Rate Limiting

Cap queries per minute per client, so one misbehaving app cannot starve the others.

Persisted Queries

Only allow pre-approved queries, the approach a large social app and a commerce service both take.
A commerce service's cost-based rate limiting: each query has a calculated "cost". Merchants get 1000 cost points per second. A simple query costs 5; a complex nested one costs 500. That prevents abuse while allowing legitimate use.

Failure timeline: unbounded nested query

Here is what happens when none of those guards exist, minute by minute.

T0  Client posts friends {{ friends {{ friends {{ ... }}}} depth 12
T1  Resolvers fan out; connection pool fills; p99 for all tenants climbs
T2  No depth/complexity limit → one query saturates the graph gateway
T3  Sibling REST/gRPC traffic shares the same DB → cascading timeouts
T4  Enforce max depth + cost score + timeout; reject before execution

Flexibility without a cost model turns the public graph into a self-DoS (denial of service) endpoint.

Correctness invariant

What must never become false

The cost guards above protect availability; this rule protects correctness. Every field a client receives was authorized for that caller, and every executed query stays inside a declared cost budget (depth, complexity, timeout, quota). Batching and caching may collapse lookups, but they must not leak another tenant's data or skip field-level authorization.

Persisted queries and the caching problem

Distinguish a trusted query allowlist from automatic persisted-query caching. An allowlist permits only operations registered by your release process. Automatic persistence may let a client submit a new document when its hash is unknown, so it saves bandwidth without automatically restricting what clients can execute. For either approach, variables still influence cost and authorization. A cached response key must include relevant variables and identity or tenant context.

The last guard in the accordion above, , also solves GraphQL's biggest caching weakness, so it earns a closer look. The idea is to ship the allowed query shapes ahead of time so production only executes known-safe plans.

Build Time: Extract & Hash

graphql
Query: { user(id: $id) { name, avatar } }
Hash: "abc123def456"

Runtime: Send Only the Hash

json
POST /graphql
{ "id": "abc123def456", "variables": { "id": "42" } }
  • Smaller payloads, because the client sends a short hash instead of the full query string.
  • No arbitrary queries reach production, since only allow-listed shapes are ever accepted.
  • The server can pre-optimize the execution plan for each known query shape.
  • A CDN can cache GET /graphql?id=abc123, because the URL now identifies the query.

Common in: large social app, commerce service, travel marketplace.

Why caching is harder than REST in the first place

That last benefit matters because ordinary GraphQL fights the HTTP caches the web is built on, which are tuned for URL-addressable GET resources.

caching (easy)

GET /users/123 → the cache key is the URL. CDN, browser, and proxy caches all work out of the box with Cache-Control: max-age=300.

GraphQL caching (hard)

POST /graphql → every query hits the same URL, a CDN cannot cache POST by default, and different queries return different shapes.

GraphQL Caching Solutions

  • Persisted queries served as GET /graphql?id=abc, which a CDN can cache by URL.
  • Normalized client cache keyed by type plus ID, for example User:123 → { name, email }.
  • Response cache at the gateway, keyed on a hash of the query plus its variables.
  • @cacheControl directive per field: name @cacheControl(maxAge: 3600), feed @cacheControl(maxAge: 60).

Scaling the graph: federation, pagination, errors, and performance

With cost and caching under control, the remaining questions are the ones that appear once a graph spans many teams and billions of requests: who owns which part of the schema, how big lists page, how errors are reported, and what the optimization stack looks like.

for microservices

Federation lets multiple subgraphs (one per team or service) compose into one graph while each team owns its slice independently.

Client→GraphQL gateway→query plan
User Service
type User { id, name, email }
Post Service
extend type User { posts: [Post] }
Comment Service
extend type Post { comments }

The gateway composes them into one unified schema. The client sees ONE API, and teams deploy independently.

Schema stitching vs federation: stitching merges schemas at runtime (the older approach). In federation, services declare relationships via the @key directive and the gateway builds a query plan (the modern approach).

Common in: large streaming service (100+ subgraphs), travel search service, vehicle platform.

Cursor pagination and query batching

Whether the graph is federated or not, huge lists need stable cursors and repeated small queries need batching to stay performant and predictable.

Cursor Pagination

graphql
query {
  user(id: "123") {
    posts(first: 10, after: "cursor_abc") {
      edges {
        cursor
        node { id, title }
      }
      pageInfo { hasNextPage, endCursor }
    }
  }
}

Query Batching (reduce HTTP round trips)

json
POST /graphql
[
  { "query": "{ user(id: 1) { name } }" },
  { "query": "{ user(id: 2) { name } }" },
  { "query": "{ product(id: 99) { price } }" }
]
// Server processes all 3 in one HTTP request.
Why cursor beats offset at billion scale: offset paging (SELECT ... OFFSET 100000) scans and discards 100K rows. Cursor paging (SELECT ... WHERE id < cursor LIMIT 10) is an index seek, O(1). Common in: large social app, developer API, commerce service (all cursor-pagination compliant).

Structured error handling

A GraphQL response can contain useful data and errors at the same time. If an optional avatar lookup fails, the product may still show the profile text. A failure in a non-null field propagates to the nearest nullable parent, which can remove more of the response than the original field. Choose nullability according to that failure behavior and have clients inspect the GraphQL error payload, rather than equating an HTTP success code with a completely successful query.

GraphQL execution can return partial data together with field errors, often using a successful HTTP status. Invalid documents, authentication failures, and requests that cannot execute can use HTTP error statuses; exact behavior also depends on the negotiated response media type. Clients should inspect both the HTTP status and the GraphQL data/errors payload instead of assuming every failure is represented by HTTP 200. See GraphQL HTTP guidance for the precise contract.

json
{
  "data": { "user": null },
  "errors": [{
    "message": "User not found",
    "extensions": { "code": "NOT_FOUND" }
  }]
}

Partial success is possible: data and errors arrive in the same response, unlike REST where a 404 means no data.

Performance at billion scale

Billions/day
large social app queries
<100ms
Avg response

The optimization stack, in order:

  1. Persisted queries, so the server skips parsing and validating query text.
  2. DataLoader batching with per-request caching, so nested lookups collapse into few queries.
  3. @defer and @stream, which send fast fields first and slow ones incrementally.
  4. Response caching at the gateway for repeated query-plus-variables combinations.
  5. A normalized client cache, so the app reuses objects it already fetched.

@defer example (progressive loading):

graphql
query {
  user { name }
  ... @defer { analytics { ... } }
}

GraphQL vs REST vs gRPC

Everything above is the GraphQL side of the story. This table puts it beside and so you can see the trade: flexibility versus caching versus typed RPC, decided by the clients you control and the operational tooling you can run.

AspectGraphQLRESTgRPC
Data fetchingClient specifies fieldsServer defines responseProto-defined messages
Over-fetchingNone (exact fields)Common (full objects)None (typed messages)
HTTP cachingHard (POST to single URL)Easy (GET + URL = cache key)Hard (binary, HTTP/2)
Best forMulti-client, complex graphsPublic APIs, simple CRUDInternal microservices
Real-worldlarge social app, developer API, commerce servicepayment API, messaging API, every APIlarge platform, large streaming service, large mobility service

How to choose REST, gRPC, or GraphQL

Choose the API style by client type, payload shape, caching needs, and operational maturity.

REST

Best default for browser and public HTTP APIs: cacheable resources, simple debugging, visible URLs, and broad infrastructure support.

gRPC

Best for controlled service-to-service calls: typed contracts, compact binary payloads, deadlines, streaming, and high request volume.

GraphQL

Best when many clients need different nested shapes: fewer round trips, less over-fetching, but harder caching and stricter query governance.

When debugging feels stuck

Split latency into client wait, gateway time, upstream fan-out, serialization, payload size, and cache behavior before changing protocols.

Simple rule of thumb

Start with REST for public clients, gRPC for busy internal RPC, and GraphQL only when nested shape flexibility is worth the caching and governance cost.

Wrap-up

You have seen the pain GraphQL removes, the cost it moves onto the server, and the controls that keep that cost bounded. Use this section to check the ideas stuck, then follow the references for the primary sources.

How to explain this in a design review

GraphQL is impressive only when you also explain how you control query cost, cache results, and keep field-level security correct. Lead with the client divergence that justifies it, then walk the cost guards and caching escapes above, and authorize at the or field boundary rather than only at the top-level query. Keep these constraints in view while you do:

  • One client request can still fan out to many slow downstream services.
  • POST-to-one-URL makes CDN caching harder unless persisted GET queries are used.
  • Mobile clients benefit from smaller payloads but can be hurt by very large nested responses.
  • Partial errors mean clients must handle data and errors together in every response.
  • Debug slow queries with resolver timings and query plans, not only HTTP latency.

Exercises

  1. Cost points. Merchants get 1000 cost points per second. Assign costs so a simple product query is ≤5 and a nested catalog browse is ≤200. What happens at the limit: an error or partial data?
  2. CDN-cached feed. You need CDN caching for a mobile home feed. Design as GET. What do you key on, and how do you invalidate after a follow?
  3. Field-level authorization. A field resolver returns email. Who may see it: self, admin, or friend? Write down where the authz check lives, and a counterexample if you only gate the root query.

Topic-specific references

What to remember

shines with multiple clients and complex nested relationships. Always cover (the fix), depth and complexity limiting, persisted queries, and when microservices need one graph. Know why caching is harder than REST, and how normalized client caches plus persisted GETs help.

Practice and further reading

Use these exercises to connect the lesson to a design decision. State your assumptions, trace what happens to one request, and explain the rule that must remain true even during a failure. That rule is the design's correctness invariant.

Focus for this lesson: Control graph-shaped query flexibility with schema governance, batching, authorization, cost limits, observability, and compatible evolution.

Explain the decision

How do you prevent GraphQL N+1 database queries?

Read the reasoning

Batch and cache resolver loads per request, expose query plans and resolver spans, and shape APIs so list fields can fetch related data in sets.

Apply it to a production scenario

A nested query is valid but saturates the database. What controls help?

Read one approach

Use persisted-query allowlists, depth/complexity budgets, pagination caps, per-field cost weights, timeouts, and tenant quotas.

Work through the numbers

A query returns 100 orders and triggers 1 customer plus 3 item lookups per order. Compare naive and batched calls.

Read the worked calculation

Naive fan-out is about 401 calls including the root; batching can reduce it to roughly four set-based calls, depending on ownership.

Reason through a failure

What must field-level authorization prevent?

Read the recovery reasoning

Adding an authorized parent field must never expose a child field the caller lacks permission to read, including through aliases and fragments.

Sources and further study

Continue with gRPC.