Resolving Schema Conflicts in Apollo Federation
As distributed API ecosystems scale, engineering teams repeatedly hit overlapping type definitions, field signature mismatches, and contested ownership during supergraph composition — and unlike a monolith, there is no single schema author to arbitrate. The cost is concrete: a single nullability drift or a duplicated @key blocks every subgraph deploy behind it, because composition is all-or-nothing. This guide sits under GraphQL Federation Architecture & Design and turns abstract conflict classes into a repeatable diagnostic and remediation workflow, covering ownership directives, field-migration patterns, and the router-level configuration that keeps composition green without sacrificing query-plan efficiency.
Two focused walkthroughs build on the material here: when to use schema stitching versus Apollo Federation for teams still choosing a composition model, and resolving field type mismatch composition errors for the single most common hard failure. Read those after you understand the conflict taxonomy below.
Prerequisites
Before working through the conflict-resolution patterns on this page, confirm the following are in place:
Concept Deep-Dive: The Conflict Taxonomy
The Apollo Federation composition engine enforces strict schema validation: it merges every subgraph SDL into one supergraph and rejects any merge it cannot prove is consistent. When rover supergraph compose fails, the output maps each conflict to the specific subgraph SDL and field that caused it. Almost every composition conflict falls into one of four families.
Type-shape conflicts. Two subgraphs define the same field on a shared type with incompatible signatures — String! versus String, Int versus Float, or divergent argument lists. Federation performs no coercion: the field types must match exactly for a shared field, or one subgraph must own the field outright. This family is covered end to end in resolving field type mismatch composition errors.
Ownership conflicts. The same field is defined in multiple subgraphs without anyone declaring @shareable, so the engine cannot tell which subgraph is authoritative. This is the INVALID_FIELD_SHARING error class, and it is fixed either by marking the field @shareable everywhere it appears or by consolidating it behind a single owner.
Key and entity conflicts. A subgraph extends an entity but does not mark the @key fields @external, or two subgraphs declare incompatible @key selections for the same type. The router cannot build a consistent _entities resolution path, so composition halts.
Directive conflicts. Mismatched @deprecated reasons, conflicting @inaccessible declarations, or an @override(from:) that names a subgraph that does not exist. These are metadata-level disagreements that nonetheless break the merged SDL.
The diagram below shows where each family is caught — boundary design errors leak into composition, composition errors leak into the query plan, and only fully-merged supergraphs reach the router.
A useful mental model is that composition is a proof obligation, not a merge: the engine must prove that for every field reachable from the supergraph root there is exactly one unambiguous resolution path. Every conflict family above is the engine refusing to guess. Type-shape and ownership conflicts are ambiguities about which subgraph answers; key and entity conflicts are ambiguities about how the router rehydrates a reference; directive conflicts are ambiguities about what the merged field even means. Internalising that distinction tells you where to look: ambiguity about who answers is fixed with @shareable or @override, ambiguity about rehydration is fixed at the @key/@external boundary, and ambiguity about meaning is fixed by aligning directive metadata. When you read a composition error, classify it into one of those three buckets first; the remediation follows almost mechanically.
A second-order benefit of resolving conflicts at the SDL contract level rather than at runtime is that the fix is verifiable in CI before any service redeploys. A stitching gateway that silently drops a field surfaces the problem as a production incident; a federation composition that cannot prove a field’s resolution path fails the check on the pull request. That shift — from runtime surprise to build-time refusal — is the entire value proposition of strict composition, and it is why the patterns below all pair an SDL change with a rover verification command.
Systematic Debugging Workflow
# Production-safe validation pipeline
rover subgraph introspect https://api.subgraph-a.com/graphql > subgraph_a.graphql
rover subgraph introspect https://api.subgraph-b.com/graphql > subgraph_b.graphql
rover supergraph compose --config supergraph.yaml 2>&1 | grep -A 5 "ERROR"
Directive & Config Spec Table
These are the directives and config keys you reach for when resolving conflicts. Composition-time effects are evaluated by the engine before any traffic flows; runtime effects are enforced by the router per request.
| Directive / Key | Syntax | Valid values | Effect (composition vs runtime) |
|---|---|---|---|
@key |
type T @key(fields: "id") |
Any resolvable field selection | Composition: defines the entity reference contract. Runtime: drives _entities fetches. |
@shareable |
field: T @shareable |
Marker (no args) | Composition: permits the same field in multiple subgraphs. Runtime: router may resolve from any owner. |
@external |
id: ID! @external |
Marker | Composition: marks a field owned elsewhere. Runtime: signals the router not to resolve it here. |
@requires |
field: T @requires(fields: "tier") |
Field selection on the same entity | Composition: declares a fetch dependency. Runtime: forces a prerequisite fetch before the local resolver. |
@provides |
field: T @provides(fields: "x") |
Field selection on a referenced type | Composition: declares opportunistic fields. Runtime: lets the router skip a hop when data is already present. |
@override |
field: T @override(from: "legacy") |
Exact registered subgraph name | Composition: transfers field ownership. Runtime: routes the field exclusively to the new owner. |
@inaccessible |
field: T @inaccessible |
Marker | Composition: keeps a field out of the API schema. Runtime: field is unqueryable through the router. |
federation_version |
federation_version: =2.9.0 |
Pinned spec version | Composition: selects the spec/diagnostics. No runtime effect. |
Step-by-Step Implementation
1. Establish ownership boundaries with @requires and @provides
Properly scoping type responsibilities prevents accidental collisions. When several services contribute to the same entity, explicit declarations dictate resolver execution order and data-fetching boundaries — the same discipline described in defining subgraph boundaries for microservices. @requires forces the query planner to fetch prerequisite fields from the originating subgraph before executing the local resolver; @provides lets a subgraph return nested fields it does not own, saving a network round-trip.
# subgraph-orders.graphql
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.9",
import: ["@key", "@external", "@requires"])
type User @key(fields: "id") {
id: ID! @external
tier: String! @external
# Requires the user's tier to compute the correct order list
orderHistory: [Order!]! @requires(fields: "tier")
}
type Order {
id: ID!
total: Float!
shippingCost: Float!
}
# subgraph-users.graphql
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.9",
import: ["@key", "@provides"])
type User @key(fields: "id") {
id: ID!
tier: String!
profile: Profile @provides(fields: "tier")
}
type Profile {
tier: String!
}
Overusing @requires increases query-plan complexity and forces sequential resolver execution. Reserve it for genuine business-logic dependencies, and prefer @provides when the originating subgraph already fetches the data, since that keeps execution parallel and reduces N+1 resolution penalties.
2. Transfer field ownership with @override
The @override directive enables intentional field migration between subgraphs without requiring immediate deprecation in the source service — essential during iterative refactoring or when migrating a legacy monolith endpoint to a dedicated service. Enforcing strict type ownership and shared schema contracts prevents regressions during the transition.
# subgraph-legacy.graphql (old owner)
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.9", import: ["@key"])
type Product @key(fields: "id") {
id: ID!
inventoryStatus: String!
}
# subgraph-inventory.graphql (new owner)
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.9",
import: ["@key", "@external", "@override"])
type Product @key(fields: "id") {
id: ID! @external
# Explicitly claims ownership from the legacy subgraph
inventoryStatus: String! @override(from: "subgraph-legacy")
}
The @override(from: "...") argument must reference the exact subgraph name registered in the supergraph config. After composition, the router routes every query for inventoryStatus exclusively to the inventory subgraph; the legacy resolver is bypassed. Plan cache invalidation deliberately: if the legacy service previously cached inventoryStatus, the new owner must use consistent cache keys to avoid stale reads during the migration window. Query-planning complexity rises marginally as the planner tracks the ownership shift, but per-request resolver overhead drops because the field is no longer fetched across services.
3. Wire a reference resolver for the contested entity
Whichever subgraph owns the entity must hydrate references the router sends it. The __resolveReference resolver receives the @key fields and returns the full entity.
import { buildSubgraphSchema } from '@apollo/subgraph';
import { gql } from 'graphql-tag';
const typeDefs = gql`
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.9", import: ["@key"])
type Product @key(fields: "id") {
id: ID!
inventoryStatus: String!
}
`;
const resolvers = {
Product: {
// Router calls this with { __typename: "Product", id } from _entities
__resolveReference(ref: { id: string }, { dataSources }: any) {
return dataSources.inventory.findById(ref.id);
},
},
};
export const schema = buildSubgraphSchema({ typeDefs, resolvers });
4. Compose the router config and verify
# supergraph.yaml — used by rover supergraph compose
federation_version: =2.9.0
subgraphs:
users:
routing_url: http://users-service:4001/graphql
schema:
file: ./users.graphql
orders:
routing_url: http://orders-service:4002/graphql
schema:
file: ./orders.graphql
inventory:
routing_url: http://inventory-service:4003/graphql
schema:
file: ./inventory.graphql
Composition Pipeline Integration
Conflict resolution only sticks if the pipeline re-checks it on every PR. Run a subgraph check against the production variant before merge, then a full compose post-merge. The same gate is documented in depth under schema validation in CI/CD pipelines, and managed-federation publishing is covered in schema registry and managed federation.
name: Composition Conflict Gate
on:
pull_request:
paths: ['subgraphs/**']
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Rover
run: |
curl -sSL https://rover.apollo.dev/nix/latest | sh
echo "$HOME/.rover/bin" >> $GITHUB_PATH
- name: Check subgraph against production
env:
APOLLO_KEY: ${{ secrets.APOLLO_KEY }}
APOLLO_GRAPH_REF: ${{ vars.APOLLO_GRAPH_REF }}
run: |
rover subgraph check "$APOLLO_GRAPH_REF" \
--name inventory \
--schema ./subgraphs/inventory/schema.graphql
Decision Guide: Which Resolution Mechanism
When two subgraphs disagree about a field, four mechanisms can resolve it, and choosing the wrong one trades one problem for another. Use this table to pick deliberately.
| Situation | Mechanism | Why | Cost |
|---|---|---|---|
| Both subgraphs legitimately compute the same value | @shareable on the field in each |
Router may resolve from whichever subgraph is already in the plan | Both must keep the value consistent forever |
| One subgraph should own the field; the other is migrating away | @override(from:) on the new owner |
Clean ownership transfer without dual deprecation | Cache fragmentation during the cutover window |
| A field is computed from data owned elsewhere | @requires on the computed field |
Forces the prerequisite fetch first | Serial execution, added latency per @requires |
| The owner already loads the data the consumer needs | @provides on the consumer’s reference |
Collapses a network hop | Only valid when the owner truly fetches the field |
The general rule: prefer the mechanism that keeps execution parallel and ownership singular. @shareable is the right default only for genuinely duplicated, cheaply-computed values like a formatted display name; reach for @override the moment a field has a clear future owner; and treat @requires as a last resort because every one you add lengthens the query plan’s critical path. None of these mechanisms substitute for getting the boundary right in the first place — revisit defining subgraph boundaries for microservices if you find yourself reaching for @requires repeatedly across the same pair of services, because that is a strong signal the bounded contexts are mis-cut.
Cross-Section Integration Points
Conflict resolution is not self-contained — it touches subgraph implementation and production operations. On the implementation side, every entity you resolve a conflict around needs a correct reference resolver; the mechanics live in implementing entity resolvers with @key directives, and the field-level patterns behind @requires are detailed in using @external and @requires for field resolution. On the operations side, an @override migration changes the router’s query plan, so its latency impact shows up in tracing and entity caching downstream. The contract you settle here is the input to those concerns: get the ownership right at composition time and the runtime behaviour becomes predictable; leave it ambiguous and you push the cost into production incidents that are far harder to diagnose than a red CI check.
Performance & Scale Considerations
Conflict-resolution choices have direct latency consequences. @requires introduces a serial fetch: the planner must resolve the required field before it can run the dependent resolver, so a chain of three @requires becomes three sequential hops. @provides does the opposite, collapsing a hop when the data is already in hand. @override reduces fan-out by removing a redundant fetch to the old owner, but watch p95 during the migration window because cache layers keyed to the old subgraph can fragment. For shared scalar-heavy entities, batch the reference resolver with DataLoader so the router’s _entities array does not become an N+1 against your datastore. The router’s demand-control directives (@cost, @listSize) let you steer the planner away from expensive join paths once conflicts are resolved and traffic patterns are known.
Failure Modes & Debugging
INVALID_FIELD_SHARING — Field "User.email" is defined in multiple subgraphs but is not marked as @shareable. Two subgraphs both resolve email. Either add @shareable to email in every subgraph that defines it, or consolidate the field behind one owner and mark it @external elsewhere.
FIELD_TYPE_MISMATCH — Field "User.email" type mismatch: expected "String!", found "String". Nullability or scalar drift across subgraphs. Federation requires exact type signatures for shared fields; align the SDL. The full walkthrough is in resolving field type mismatch composition errors.
OVERRIDE_SOURCE_HAS_OVERRIDE / unknown from — @override source subgraph "subgraph-legacy" does not exist. The from: argument does not match a registered subgraph name. Names are case-sensitive and must match supergraph.yaml exactly.
Circular @requires — composition rejects the merge when Service A requires a field from Service B that requires a field from Service A. Flatten the dependency by moving one of the computed fields to the subgraph that already owns its inputs, or denormalize the required value onto the entity.
Frequently Asked Questions
How does Apollo Router handle conflicting field types during composition?
The composition engine applies strict type unification. If two subgraphs define the same field with incompatible signatures — Int versus String, or differing nullability — composition fails immediately with FIELD_TYPE_MISMATCH. There is no automatic coercion or fallback; you must align the SDLs or designate a single owner with @override.
Can I resolve schema conflicts without modifying existing subgraph schemas?
Short-term workarounds include client-layer field aliasing or a compatibility shim subgraph that proxies and transforms conflicting responses. Both add maintenance debt and latency, so treat them as bridges. Durable resolution requires explicit schema alignment and an ownership migration with @override.
What is the performance impact of using the @override directive?
@override simplifies the query plan by eliminating the redundant fetch to the legacy subgraph, which usually lowers request latency once the migration stabilises. The transient cost is cache fragmentation during the cutover window, since clients may hold data keyed to the old service. Monitor cache hit rates and p95 until the old owner is fully drained.
How do I prevent composition conflicts from reaching staging?
Run rover subgraph check on every PR against the production variant and block merges on FAILURE severity. Reserve full rover supergraph compose for post-merge so PR latency stays low, and keep a rollback procedure that reverts to the last valid supergraph snapshot.