Why One Graph Stops Working at Team Scale
Why One Graph Stops Working at Team Scale
A five-person team can keep one GraphQL schema coherent because somebody holds the whole graph in their head. At forty engineers across catalog, checkout, and accounts that same schema becomes a shared build artifact: every team reviews every schema change, every release ships every resolver, and one runaway query in catalog can starve checkout because both run in one process against one connection pool.
Federation removes the coupling without removing the single graph clients depend on. Each team runs a subgraph — an ordinary GraphQL server with its own schema, resolvers, datastore, and release cadence. A build step composes every subgraph schema into one supergraph, and a gateway serves that supergraph as the only endpoint clients talk to.
before after
+-------------------------+ client --- one endpoint, one schema
| one schema | |
| one resolver map | v
| one deploy, one DB | +--------------+
| 40 engineers commit | | gateway | plans, then fans out
+-------------------------+ +--------------+
| | |
catalog checkout accounts
subgraph subgraph subgraph
(own DB) (own DB) (own DB)
Clients see no difference: one endpoint, one document, one type system, one error shape. Ownership changes on the inside. The catalog team edits catalog types and deploys catalog alone, and a slow checkout query cannot exhaust catalog's database because they are separate processes with separate resources.
Subgraphs do not import each other's resolvers, migrations, or tables. The only thing they share is type definitions — and only the subset needed to reference each other, which the next topic defines precisely. Federation buys that team autonomy at the price of a composition step in CI, a gateway you now have to operate, and at least one extra network hop per operation. Whether the trade is worth it depends on team topology more than on request volume; see stitching versus federation for the lighter alternative and the org shapes where it wins.
The Four Pieces and One Request Path
The Four Pieces and One Request Path
Four nouns carry the entire architecture:
| Piece | What it is | Who operates it |
|---|---|---|
| Subgraph | a standalone GraphQL server with its own schema and data | one product team |
| Composition | a build step that merges subgraph schemas or fails loudly | CI |
| Supergraph | the composed schema, including routing metadata | produced artifact |
| Gateway | validates, plans, caches, and fans out every operation | platform team |
Take a product page query that spans three subgraphs:
query ProductPage($id: ID!) {
product(id: $id) { # products subgraph
name # products subgraph
reviews { # reviews subgraph, via entity hop
rating
author { # accounts subgraph, second hop
displayName
}
}
}
}
The gateway does not forward that document verbatim to anyone. It plans first:
client --query--> gateway
|-- fetch 1 --> products { product(id: $id) { id name } }
|-- fetch 2 --> reviews representations: [{Product, id}] after 1
|-- fetch 3 --> accounts representations: [{User, id}] after 2
<-- merged response shaped exactly like the original query ---
Each fetch is a plain GraphQL operation against one subgraph. Subgraphs never call each other; only the gateway fans out, which keeps the cross-service traffic in one place where it can be planned, traced, cached, and rate limited. Fields the gateway already has — Product.id from fetch 1 — are shipped to reviews as an entity representation instead of being fetched again.
Two consequences matter in production. First, an operation touching three subgraphs costs at least three sequential round trips, so cross-subgraph depth shows up directly in latency. Second, because planning happens before execution, an operation whose shape the gateway has seen before is served from a plan cache rather than re-planned, and that cache hit rate is one of the first numbers worth watching.
Drawing Ownership Lines, and When Not to Federate
Drawing Ownership Lines, and When Not to Federate
Good boundaries follow two forces: who owns the data, and how often the thing changes. Boundaries drawn by entity count produce subgraphs that must deploy together anyway, which reintroduces the coupling federation was supposed to remove.
Rules that hold up in practice
- One team owns one subgraph. Shared ownership of a subgraph is the old monolith with extra latency.
- Every field has exactly one owning subgraph. Two owners means composition cannot decide who resolves it.
- A type that appears in more than one subgraph needs a key so the gateway can hop between them.
- Avoid cycles where A's entity requires B's, which requires A's. The plan degenerates into long sequential chains.
- Two subgraphs that always ship in the same release should be one subgraph until they stop.
What federation costs you
- The gateway becomes critical path infrastructure: it needs redundancy, a warmed plan cache, and its own dashboards.
- A schema registry or at minimum a CI composition step becomes mandatory, because composition failures otherwise surface at deploy time.
- Local development gets heavier — you now run several subgraphs plus a gateway to exercise a single page.
- Debugging spans processes: a single client operation becomes several traces, so distributed tracing stops being optional.
When to skip it. One team, one or two services, one deploy unit: a modular monolith gives you the same code-level boundaries with none of the infrastructure, and you can always extract later. Federation earns its keep when multiple teams need to change their slice of the graph without negotiating a shared release. If you cannot name a second team that would benefit, you do not need federation yet — and you will pay the gateway tax for nothing.
Interviewers like to probe exactly this boundary. A strong answer names both sides: federation is an organizational scaling tool first and a performance tool never — it adds hops, so it makes individual operations slower while making teams faster.
Practice Problems
You own the migration of a monolithic storefront schema into federation. Split the types below into three subgraphs (catalog, reviews, accounts) following team ownership, and mark every type referenced across a boundary with a key so the gateway can hop to it.
Example:
Input: type Query { product(id: ID!): Product reviewsFor(productId: ID!): [Review!]! user(id: ID!): User } type Product { id: ID! name: String! priceCents: Int! } type Review { id: ID! rating: Int! body: String! author: User! } type User { id: ID! displayName: String! email: String! }
Output: # catalog subgraph type Query { product(id: ID!): Product } type Product @key(fields: "id") { id: ID! name: String! priceCents: Int! } # reviews subgraph type Query { reviewsFor(productId: ID!): [Review!]! } type Product @key(fields: "id") { id: ID! reviews: [Review!]! } type Review { id: ID! rating: Int! body: String! author: User! } type User @key(fields: "id") { id: ID! } # accounts subgraph type Query { user(id: ID!): User } type User @key(fields: "id") { id: ID! displayName: String! email: String! }
Each root field stays with the team that owns its data. Product is defined in two subgraphs, so both declare @key(fields: "id"); reviews reaches Product by reference and adds its own field, accounts owns User and reviews only declares the key fields it needs to hand author references onward.
Optimal Solution — O(1) per boundary check time, O(T) for T types space
Group root fields and object fields by owning team, then promote every cross-boundary type to an entity by adding @key(fields: "id") in each subgraph that mentions it.
# catalog
type Query { product(id: ID!): Product }
type Product @key(fields: "id") { id: ID! name: String! priceCents: Int! }
# reviews
type Query { reviewsFor(productId: ID!): [Review!]! }
type Product @key(fields: "id") { id: ID! reviews: [Review!]! }
type Review { id: ID! rating: Int! body: String! author: User! }
type User @key(fields: "id") { id: ID! }
# accounts
type Query { user(id: ID!): User }
type User @key(fields: "id") { id: ID! displayName: String! email: String! } Edge Cases:
- A field needed by two teams (Product.name) lives in one subgraph; the other reaches it through the entity hop
- email must stay in accounts only, or authorization on it becomes unenforceable in one place
- Moving Review.author from a plain field to an entity reference changes no client query — only the plan
Quiz
1. Where do production clients send their GraphQL operations in a federated graph?
2. When does composition run in a federation setup?
3. A query selects fields from three subgraphs, and reviews are reached through an entity hop. How many subgraph fetches should you expect?
4. Why not let clients call each team’s GraphQL server behind a load balancer instead of running a gateway?
Flashcards
Question
Subgraph
Click to reveal answer
Answer
A standalone GraphQL server owning one slice of the graph: own schema, resolvers, datastore, and deploy cadence.
Question
Supergraph
Click to reveal answer
Answer
The composed schema served by the gateway: client-visible types plus join metadata recording which subgraph owns each field.
Question
Gateway (router)
Click to reveal answer
Answer
The process that validates, plans, caches, and fans out operations to subgraphs, then merges partial results.
Question
Composition
Click to reveal answer
Answer
Build-time merge of subgraph schemas into a supergraph; either succeeds deterministically or fails with errors naming subgraph, type, and field.
Question
Cost of federation
Click to reveal answer
Answer
Extra network hops per operation, a gateway to operate, and CI composition — you trade per-request latency for team autonomy.
Revision Notes
Key Takeaways
- 1. Clients keep one endpoint and one type system; only ownership and deploys split into subgraphs
- 2. The gateway plans before it forwards, so fetch count follows field ownership, not field count
- 3. Federation is an organizational scaling tool: it makes teams faster and individual operations slightly slower
Interview Tips
- • Walk the request path out loud: client → gateway plan → sequential/parallel subgraph fetches → entity hops → merged response
- • Answer "when would you not use federation" with a concrete org shape: one team, one deploy unit, modular monolith instead
Cheat Sheet
Pieces
- Subgraph: standalone GraphQL server, one owning team
- Composition: build-time merge in CI, output = supergraph
- Supergraph: API schema + join routing metadata
- Gateway: validate → plan → cache plan → fan out → merge
Request path
client → gateway → fetch products { id name }
→ fetch reviews (representations: Product{id})
→ fetch accounts (representations: User{id})
← merge into the client's original shape
Ownership rules
- One team per subgraph; one owner per field
- Shared type → declare a @key so the gateway can hop
- Subgraphs never call each other; only the gateway fans out
- Two subgraphs that always deploy together should be one subgraph
Skip federation when: one team, ≤2 services, no shared release pain → modular monolith.