How to Migrate from a GraphQL Monolith to Federation

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.
When your GraphQL API is small, a single codebase works fine. One schema, one deployment, one team. But as your organization grows, the monolith starts to pull in two directions: every team needs to move fast on their own domain, yet the schema is shared by all of them. A change to the Product type requires coordination across the catalog team, the reviews team, and the orders team. Deployments become serialized. Schema reviews become political.
This is the problem federation is designed to solve, and migrating from a monolith to federation is more tractable than it looks.
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:
This looks manageable. But notice what's hidden: the resolver for Product.reviews is owned by the reviews team, yet it lives alongside resolvers owned by the catalog team, the orders team, and the identity team. Nobody has clear ownership of the boundary between Product and Review. A schema change by one team can break another's resolvers. Deploying a fix to the reviews resolver requires the entire monolith to redeploy.
The N+1 problem is also harder to contain here. Without clear domain boundaries, it's easy to add a nested resolver that fires a database query per parent object, and at the same time, it's hard to catch it until it's already in production.
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. An entity is a type that can be referenced and extended by other subgraphs.
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.
For a story-driven walkthrough of how this pattern appears in GraphQL Federation, see Scaling GraphQL Federation: Inside Cosmo's Food Park.
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):
The reviews subgraph does not own Product. It only knows how to look up reviews given a product ID. The router assembles the complete Product object from the products and reviews subgraphs before returning a response.
When a client sends a query that spans all four subgraphs:
The router builds an execution plan: fetch the user from the users subgraph, fetch the user's orders from the orders subgraph, then fetch the product name and reviews from the products and reviews subgraphs. In this example, those last two fetches can run in parallel because both depend only on Product.id. However, many real-world queries have deeper dependency chains and execute in more stages than this best-case illustration suggests.
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 (with v1 compatibility where needed). The initial step can be a pass-through: the router sits in front and forwards traffic while you establish the seam you'll peel from, often without changing application code yet. That is not the same as full federated participation β once the monolith must participate in entity resolution and cross-subgraph references, you add federation annotations such as @key, _entities support, and __resolveReference handlers as needed.
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:
In Cosmo, that schema change should be validated with wgc subgraph check, which checks both breaking changes and composition errors for the affected federated graphs before rollout.
@override(from: "monolith") tells composition to route Product.reviews to this subgraph instead of the monolith. Run wgc subgraph check before deploying to catch both composition errors and breaking changes across the connected federated graphs associated with that subgraph:
If the check passes, publish the new subgraph. Clients can remain unaffected when the schema contract is unchanged and resolver behavior matches what they already depend on. However, differences in nullability, timing, or data consistency can still surface edge cases, so validate against real client queries.
Phase 2 β Roll out progressively. @override changes field ownership at composition time; gradual cutover is a separate concern. Before routing all traffic to the new subgraph, use Cosmo Feature Flags to introduce it as a feature subgraph that conditionally replaces the original subgraph for selected traffic. Cosmo supports shadow mode for migration scenarios, so you can compare correctness and performance before exposing responses to clients, then roll out progressively to 1%, 10%, and 100% of users.
The router itself does not split traffic; in Cosmo's model, traffic selection is typically controlled by a load balancer, header, cookie, or other request context such as X-Feature-Flag.
Phase 3 β Decommission the old code. Once the new subgraph is stable at 100% of traffic, mark obsolete monolith fields as @inaccessible to remove them from the client-facing API schema during staged cleanup, which is useful when you need to hide a field before every subgraph has stopped referencing it. If nothing in the supergraph references the field anymore, you can delete it directly. After confirming no active clients depend on the old path, remove the resolver code and schema definitions from the monolith.
Repeat Phases 1β3 for each bounded context: orders, products, users. When the monolith no longer owns any fields, remove it from the router config.
Federation adds a new category of failure: composition errors. Two subgraphs can each be valid GraphQL, yet fail to compose into a valid supergraph. For example, if both the products and reviews subgraphs define Product.price without marking it @shareable, composition will fail with a conflict error for that field. Non-key fields defined in multiple subgraphs must be marked @shareable, or otherwise coordinated via directives like @external depending on ownership.
Catch composition errors before they reach production by running wgc subgraph check in CI on every pull request:
This will report breaking changes separately from composition errors, which helps distinguish client-facing contract breaks from federation wiring mistakes before the change lands in production. Example output:
| CHANGE | TYPE | DESCRIPTION | BREAKING |
|---|---|---|---|
| BREAKING | FIELD_REMOVED | Field 'price' was removed from 'Product' | |
| NON-BREAKING | FIELD_ADDED | Field 'discountedPrice' was added to 'Product' |
Cosmo Schema Contracts let you derive filtered subsets of a federated graph for different audiences by annotating schema elements with @tag. A common pattern is to create a public contract that excludes internal tags, but the broader idea is audience-specific graph subsets rather than only a single public-versus-internal split.
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. But that benefit depends on the plan: some federated queries still execute in stages because entity resolution introduces dependencies between subgraphs. Compared with a monolith, federation trades in-process calls for network hops and planning overhead, so the real answer is workload-dependent rather than universally faster.
For simple single-domain queries, there is an additional network hop between client and router, and another between router and subgraph. In low-latency environments (same region or VPC), that overhead is often single-digit milliseconds, but cross-region topologies, many hops, or slow subgraphs can add significantly more. Routers typically cache query plans for repeated operations, which reduces planning overhead for common requests, but end-to-end latency still depends on the number of subgraph fetches, their dependency order (query plan depth), and subgraph performance.
Federation doesnβt eliminate N+1 inside a subgraph, each subgraph still needs its own batching strategy, typically DataLoader. What federation does give the router is a structured way to perform entity resolution across subgraphs, which can reduce cross-subgraph fan-out when entity fetches are batched effectively. For tracing, per-subgraph latency, and field-level metrics across a federated graph, see OSS Analytics, Monitoring, and Tracing for Federated GraphQL APIs.
The god-subgraph. One team ends up owning 60β80% of all types. This recreates the monolith problem, just inside federation. Subgraph boundaries should align with ownership boundaries. These are often team-based, though a team may own multiple subgraphs and org models vary. If one subgraph is growing while others are thin, that is a signal to split.
One subgraph per data source. Mapping a subgraph to a database (the "users_db subgraph") rather than a domain mixes infrastructure concerns into your API boundary. The subgraph should reflect what the team owns, not which Postgres cluster they use.
Overusing @shareable to avoid decisions. @shareable lets multiple subgraphs resolve the same field and is valid when both subgraphs can return an identical value independently. A good example is Product.currency cached in the orders subgraph so receipt rendering doesn't require a cross-subgraph fetch. Using it to defer ownership ("both teams can resolve it, figure it out later") leads to inconsistencies and split-brain data issues.
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. Federation pays off when team ownership is the bottleneck. Until then, a well-structured monolith is easier to operate.
Cascading @requires chains. If resolving a field requires data from subgraph A, which requires data from subgraph B, which requires data from subgraph C, you have built a sequential dependency chain that adds latency at every link. Flatten these chains when possible or rethink the entity model. For a deeper look at how @requires works and when to reach for it, see Microservice Dependencies: How @requires Makes Them Explicit.
Federation is not the right answer for every team. You probably do not need it if:
- Your entire GraphQL API is owned and deployed by a single team.
- Your schema has fewer than a handful of distinct domains.
- Your bounded contexts are still changing rapidly. If subgraph boundaries are drawn too early, they become expensive to move.
In these cases, a well-structured monolith with clear module boundaries is simpler to operate, easier to debug, and faster to change. You can always introduce federation later when team ownership becomes the actual bottleneck. For a look at how to preserve design coherence once you do make the switch, see Design Like a Monolith, Implement as Microservices.
The table below mixes core federation directives such as @key, @override, and @shareable with additional directives currently supported by WunderGraph Cosmo, including authorization directives.
| 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 β useful for internal fields or staged removal across subgraphs |
@external | Marks a field this subgraph references but does not resolve β required for use in @requires or @provides |
@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 when resolving a field, allowing the router to skip additional fetches in some query plans |
@authenticated | Cosmo-supported federation directive that marks a field or type as requiring authentication |
@requiresScopes | Cosmo-supported federation directive that restricts access based on JWT scopes |
For a full walkthrough of field-level authorization with @authenticated and @requiresScopes, see Field Level Authorization for GraphQL Federation with Cosmo Router.
State of GraphQL Federation 2026
How are teams governing schema changes, handling production traffic, and measuring Federation success? Share your experience and get early access to the full report. For every valid survey completed, we'll donate $30 to UNICEF .
Moving from a monolithic GraphQL API to a federated one is a significant shift in how you think about schema ownership. The goal is not to adopt a new technology for its own sake but to match your API's structure to your team's structure, allowing teams to ship independently without coordinating on every schema change.
The strangler-fig pattern makes this migration tractable. You do not need to rewrite everything at once. Place a router in front of the monolith, peel off one subgraph at a time using @override, validate each step with wgc subgraph check, and use Cosmo Feature Flags (or equivalent traffic controls) to roll out gradually. Each phase is reversible, each boundary is testable, and the risk of any single step is bounded.
Federation works best when team ownership is your actual bottleneck. If it is, the investment pays off quickly.
Frequently Asked Questions (FAQ)
Use the strangler-fig pattern. Place a federation router in front of your monolith as a single subgraph, then carve off one bounded context at a time using @override to redirect fields to new subgraphs. Each phase ships independently, and you can stop at any point. Existing clients can remain unaffected if schema contracts and resolver behavior are preserved.
An entity is a type decorated with @key that can be referenced and extended across multiple subgraphs. The router uses the key fields to fetch the type's data from whichever subgraph owns it, then merges the result into a single response.
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. The two are complementary: each microservice can own one or more subgraphs.
It depends on the query. Federated queries that span multiple subgraphs require network hops between the router and each subgraph. However, the router can execute independent subgraph fetches in parallel when the query plan allows it, which can improve performance for some complex cross-domain queries. In other cases, entity dependencies, network hops, and subgraph latency can outweigh that benefit.
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. Start with a monolith and federate when team ownership becomes the bottleneck.
@override(from: "subgraph_name") is a Federation v2 composition directive: it tells composition that a field should now resolve from the current subgraph instead of the previous one. Gradual rollout during migration is separate β in Cosmo, you pair the schema change with Feature Flags, headers, or load-balancer rules to shift traffic before fully retiring the old resolution path.
CCO & Co-Founder at WunderGraph
Stefan Avram is the CCO and one of the co-founders of WunderGraph, helping enterprise customers adopt and scale federated architecture. A former software engineer, he translates technical value into practical outcomes and shaped WunderGraph's early customer motion, guiding platform teams from onboarding to production in demanding environments. A former college soccer player, he brings a competitive, team-driven mindset to every stage of customer growth, with a focus on helping engineering-led organizations move fast without losing control.

