# GraphQL — Part 2: N+1, DataLoader, Over/Under-Fetching, and Evolution

Part 1 covered the schema, queries/mutations, and the resolver-per-field execution model — including flagging that independent per-field resolvers are the root cause of this part's first topic. This part assumes Part 1.

## 1. The N+1 Problem

Consider a query listing orders, each with its customer's name:

```graphql
query { orders(customerId: "c1") { id customer { name } } }
```

With Part 1's naive resolver model, if `orders` returns 50 rows, the engine calls `Order.customer`'s resolver **separately, once per order** — 50 individual `findById` calls, plus the 1 call that fetched the orders list: **N+1 queries** for a list of N items, even though every `customer` lookup could have been one batched query:

```ts
// NAIVE — fires one DB query per order, 50 orders = 50 extra round trips
Order: {
  customer: (parent) => ctx.db.customers.findById(parent.customerId),
}
```

This is the GraphQL-specific flavor of the same N+1 problem covered generically in `databases/indexing-storage/05-n_plus_1_query_problem` — GraphQL doesn't cause it any more than an ORM does, but GraphQL's per-field resolver model makes it the *default* behavior unless explicitly fixed, which is why it comes up so often in GraphQL-specific discussions.

## 2. DataLoader: Batching and Caching Within One Request

The standard fix is **DataLoader** — a per-request utility that collects every individual load call issued during one tick of the event loop, then fires **one** batched query for all of them:

```ts
import DataLoader from "dataloader";

function createCustomerLoader(db: Db) {
  return new DataLoader<string, Customer>(async (customerIds) => {
    // called ONCE per request-tick with ALL the ids collected so far, e.g. ["c1","c2","c3",...]
    const rows = await db.customers.findByIds(customerIds); // ONE query, WHERE id IN (...)
    const byId = new Map(rows.map((r) => [r.id, r]));
    return customerIds.map((id) => byId.get(id)!); // MUST return results in the SAME order as customerIds
  });
}

// resolver: looks identical to the naive version...
Order: {
  customer: (parent, _args, ctx) => ctx.loaders.customer.load(parent.customerId), // ...but .load() is batched under the hood
}
```

Even though `Order.customer`'s resolver still looks like it's called once per order, `DataLoader.load()` doesn't hit the database immediately — it queues the id and returns a Promise, and on the next microtask tick, every queued id fires together in **one** `findByIds` call. This turns the 50 individual lookups from section 1 into exactly 2 queries total (orders + one batched customer lookup), without changing the shape of the resolver code at all. `DataLoader` also caches within a single request by default, so requesting the same id twice in one query never re-fetches it.

![Naive resolvers issue one query for posts plus one per author; a per-request DataLoader collects author ids in one tick and loads them in a single IN query](diagrams/01-n-plus-one-dataloader.svg)

## 3. Over-Fetching and Under-Fetching, Solved by Construction

- **Over-fetching**: a REST endpoint returning a fixed shape that includes fields the client doesn't need (e.g. `GET /orders/482` always including a large `items` array even when the client only wanted `status`) — GraphQL's client-specified field selection (Part 1, section 1) eliminates this by construction, since the client only ever gets what it asked for.
- **Under-fetching**: a REST client needing data from multiple endpoints to render one view (`GET /orders/482` then `GET /customers/7` to get the customer's name) — GraphQL's resolver graph (Part 1, section 4) resolves this in one request, one round trip, regardless of how many "REST endpoints" worth of data it touches.

This is the core pitch for GraphQL over REST for client-driven UIs with varied data needs (e.g. a mobile app and a web dashboard wanting different subsets of the same underlying data) — each client sends its own query shaped exactly to what it renders, without the server needing bespoke per-client endpoints or a REST `?fields=` query-param workaround.

## 4. Subscriptions: a Third Root Type

Beyond Query and Mutation, GraphQL defines **Subscription** for long-lived, server-push updates over a persistent connection (typically WebSockets):

```graphql
type Subscription {
  orderStatusChanged(orderId: ID!): Order!
}
```

```ts
subscribe: () => pubsub.asyncIterator(`ORDER_STATUS_CHANGED_${orderId}`),
```

A client subscribes once and receives a new payload every time the server publishes an update matching that subscription — conceptually similar to `networking-web/browser-security/04-websockets`'s persistent-connection model, with GraphQL's type system and field-selection layered on top of the transport.

## 5. Schema Evolution: Deprecation Instead of URL Versioning

REST versions by URL path (`backend/api-design/05-api_versioning`'s `/v1/orders` -> `/v2/orders`). GraphQL, with its single endpoint, versions **fields**, not the API as a whole:

```graphql
type Order {
  total: Int! @deprecated(reason: "Use totalCents instead — this field will be removed on 2026-01-01.")
  totalCents: Int!
}
```

A deprecated field keeps working for existing clients (their queries still select `total` and still get a value) while tooling (schema introspection, IDE autocomplete) surfaces the deprecation warning to anyone writing a new query against it. This avoids REST's all-or-nothing "the whole API moved to `/v2`" — individual fields evolve independently, and a client's query only breaks if it explicitly selects a field that's been fully removed after its deprecation window closes.

## Common Pitfalls

1. **Shipping a resolver-per-field design without DataLoader.** Section 1's N+1 is the default outcome, not an edge case — any list-of-objects-with-a-nested-relation query hits it immediately at real data volumes.
2. **A DataLoader batch function returning results out of order.** `DataLoader` matches results back to callers positionally — the batch function's return array MUST be the same length and same order as the ids it was called with, or wrong data gets attached to wrong callers.
3. **Treating GraphQL as automatically faster than REST.** It solves over/under-fetching at the network-request level; it does nothing by itself about the underlying database work — an unbatched N+1 resolver graph can be *slower* than a well-designed REST endpoint doing one JOIN.
4. **Removing a field instead of deprecating it first.** Breaks every client still selecting it with no warning — deprecation exists specifically to give clients a migration window.
5. **Forgetting `Subscription` needs its own transport concerns.** Unlike Query/Mutation over plain HTTP POST, Subscriptions need a persistent connection (WebSocket) and its own auth/origin-validation story (`networking-web/browser-security/04-websockets`).

## Quick Reference

```
N+1               -- naive per-field resolver = 1 query per parent row in a list
DataLoader.load()  -- queues + batches all loads in one tick into ONE query, caches per-request
Over-fetching       -- fixed REST shape includes unwanted fields -- solved by client field selection
Under-fetching       -- REST needs multiple endpoints for one view -- solved by resolver graph, one request
Subscription           -- 3rd root type, server-push over a persistent connection (WebSocket)
Schema evolution         -- @deprecated on fields, NOT URL versioning -- fields evolve independently
```
