How to Migrate from a GraphQL Monolith to Federation - WunderGraph
Co-Founder at WunderGraph
November 17, 2023·12min read
Last updated on May 20, 2026
TL;DR
A GraphQL monolith becomes a bottleneck when different teams all need to change the same schema. Federation solves this by splitting the schema into independent subgraphs composed at runtime by a router. You can migrate incrementally using the strangler-fig pattern: put a router in front of the monolith, then peel off subgraphs one bounded context at a time using @override. No big-bang rewrite required.
The Limitations of Monolithic GraphQL Architecture
In a monolithic GraphQL system, every type and every resolver lives in the same process. This works well at first because there is no network overhead between resolvers, and the entire schema is visible in one place. But as the system grows, the tight coupling creates real friction.
Consider a simplified e-commerce monolith. All four domains — users, products, reviews, and orders — live in a single schema:
# monolith: schema.graphql
type Query {
user(id: ID!): User
product(id: ID!): Product
}
type User {
id: ID!
name: String!
orders: [Order!]!
}
type Product {
id: ID!
name: String!
price: Int!
reviews: [Review!]!
}
type Order {
id: ID!
total: Int!
product: Product!
}
type Review {
id: ID!
rating: Int!
body: String!
}
How Federated Architecture Addresses This
Federated architecture breaks the monolith into smaller, independently deployable subgraphs. Each subgraph owns a slice of the schema and runs as its own service. A federation router sits in front of all subgraphs, composes their schemas into a single supergraph, and routes each client query to the right subgraphs.
Here is the same e-commerce schema split across four subgraphs. Each subgraph uses the @key directive to declare its owned types as entities.
# users subgraph
type User @key(fields: "id") {
id: ID!
name: String!
email: String!
}
type Query {
user(id: ID!): User
}
# products subgraph
type Product @key(fields: "id") {
id: ID!
name: String!
price: Int!
}
type Query {
product(id: ID!): Product
}
# reviews subgraph — extends Product without owning it
type Product @key(fields: "id") {
id: ID!
reviews: [Review!]!
}
type Review {
id: ID!
rating: Int!
body: String!
}
# orders subgraph — extends User, references Product without resolving its fields
type User @key(fields: "id") {
id: ID!
orders: [Order!]!
}
type Order {
id: ID!
total: Int!
product: Product!
}
# resolvable: false — this subgraph can reference Product by key but cannot serve as the origin for entity resolution; it still participates when the entity is already resolved elsewhere
type Product @key(fields: "id", resolvable: false) {
id: ID!
}
Each team ships independently. The reviews team can deploy a change to Review without touching the products subgraph. The router handles the coordination at runtime.
How Entity Resolution Works
The mechanism that makes cross-subgraph queries possible is entity resolution. When the router needs fields from multiple subgraphs for the same type, it sends _entities queries to the subgraphs that need to resolve additional fields for that entity, passing the key values as representations.
Here is what the resolver looks like in the reviews subgraph (TypeScript, using @apollo/subgraph):
const resolvers = {
Product: {
// The router passes { __typename: "Product", id: "p1" }.
// Return an object containing the key (and optionally additional fields) so downstream field resolvers can perform their lookups.
__resolveReference: ({ id }: { id: string }) => ({ id }),
reviews: ({ id }: { id: string }) => reviewsByProductId(id),
},
};
When a client sends a query that spans all four subgraphs:
query UserDashboard($userId: ID!) {
user(id: $userId) {
name
orders {
total
product {
name
reviews {
rating
body
}
}
}
}
}
GraphQL Federation Migration Playbook
The safest way to migrate from a GraphQL monolith to federation is the strangler-fig pattern: wrap the monolith first, then incrementally replace it from the outside in.
Phase 0 — Wrap the monolith.
Register your existing monolith behind Cosmo Router, which is open source, Apache 2.0 licensed, and supports GraphQL Federation v2.
Phase 1 — Extract a bounded context.
Pick one domain (reviews is a good candidate because it is self-contained) and build a new reviews subgraph. Use @override to declare that the new subgraph now owns fields previously resolved by the monolith:
# reviews subgraph — taking ownership of Product.reviews from the monolith
type Product @key(fields: "id") {
id: ID!
reviews: [Review!]! @override(from: "monolith")
}
Phase 2 — Roll out progressively.
Phase 3 — Decommission the old code.
Schema Governance and CI
Federation adds a new category of failure: composition errors. Two subgraphs can each be valid GraphQL, yet fail to compose into a valid supergraph. Non-key fields defined in multiple subgraphs must be marked @shareable, or otherwise coordinated via directives like @external depending on ownership.
Does GraphQL Federation Add Latency?
It depends. For queries that span multiple domains, a federated router can reduce latency when the query plan contains independent fetches that can run in parallel.
GraphQL Federation Anti-Patterns to Avoid
The god-subgraph. One team ends up owning 60–80% of all types.
One subgraph per data source. Mapping a subgraph to a database mixes infrastructure concerns into your API boundary.
Overusing @shareable to avoid decisions.
Premature federation. Splitting a monolith before your team structure and bounded contexts are stable means you will spend more time re-drawing subgraph boundaries than shipping features.
Cascading @requires chains.
When Not to Federate
Federation is not the right answer for every team.
Cosmo Federation Directives Quick Reference
| Directive | Purpose |
|---|---|
@key |
Marks an entity and its identifying fields |
@override |
Moves a field's resolution from one subgraph to another |
@shareable |
A field can be resolved by multiple subgraphs |
@inaccessible |
Hidden from the client-facing API schema |
@external |
Marks a field this subgraph references but does not resolve |
@requires |
This subgraph needs additional fields from another subgraph to resolve a field |
@provides |
Indicates that this subgraph can supply specific fields of a referenced entity |
@authenticated |
Marks a field or type as requiring authentication |
@requiresScopes |
Restricts access based on JWT scopes |
Conclusion
Moving from a monolithic GraphQL API to a federated one is a significant shift in how you think about schema ownership. The goal is to match your API's structure to your team's structure, allowing teams to ship independently without coordinating on every schema change.
Frequently Asked Questions (FAQ)
How do I migrate from a GraphQL monolith to federation without a big-bang rewrite?
Use the strangler-fig pattern.
What is an entity in GraphQL Federation?
An entity is a type decorated with @key that can be referenced and extended across multiple subgraphs.
What is the difference between federation and microservices?
Microservices is a deployment pattern — independent services with their own processes and databases. GraphQL Federation is an API composition pattern — independent subgraphs that compose into a single unified schema.
Does federation add latency?
It depends on the query.
When should I not use federation?
If your team is small (under three or four engineers), if your bounded contexts haven't stabilized yet, or if your entire GraphQL API is owned by one team, federation adds operational overhead without meaningful benefit.