What this usually means
These errors almost always stem from a mismatch between what the gateway expects and what the subgraphs provide. In schema stitching (Apollo Federation or similar), the gateway composes a supergraph from multiple subgraph schemas. The most common root cause is that a type is defined in multiple subgraphs without proper @key or @external annotations, or the same field is declared with different types across subgraphs. Another frequent issue is stale gateway state: the gateway caches the composed schema, and if subgraphs update without restarting the gateway, it serves an outdated schema. Less common but brutal: directive definitions in subgraphs that conflict with the gateway's built-in directives (e.g., @deprecated, @skip, @include) or custom directives not propagated correctly. When you see 'must be defined only once', the real problem is almost always a missing @external or @key on a shared type, causing the composition engine to see two separate definitions instead of one entity.
The first ten minutes — establish facts before touching code.
- 1Run the gateway's schema composition command (e.g., `rover supergraph compose --config supergraph.yaml`) to get the exact composition error with line numbers.
- 2Check the gateway logs for 'Schema composition error' or 'Validation error' — these often contain the exact type and field names.
- 3Use `curl` to query the gateway's `_service { sdl }` from each subgraph endpoint to fetch the actual SDL being served.
- 4Compare the SDL from each subgraph with what's defined in the code — look for differences in types, fields, and directives (especially @key, @external, @requires).
- 5Restart the gateway and immediately run a query that triggers the federated entity resolution — if the error disappears temporarily, it's a caching/state issue.
The specific files, logs, configs, and dashboards that usually own this bug.
- searchGateway config file (supergraph.yaml or federation config) — verify subgraph URLs and schema definitions.
- searchSubgraph GraphQL schema files (e.g., `type-defs.js` or `.graphql` files) — check for duplicate type definitions across subgraphs.
- searchGateway's composed schema cache (often an in-memory map or a Redis key) — inspect its content and TTL.
- searchSubgraph's `_service` SDL endpoint — fetch with `curl -X POST -H "Content-Type: application/json" -d '{"query":"{ _service { sdl } }"}'`.
- searchCI/CD pipeline logs — compare schema composition output between branches or commits.
- searchGraphQL gateway error logs (e.g., `/var/log/gateway/error.log`) — look for stack traces from composition or validation.
Practical causes, not theory. These are the things you will actually find.
- warningSame type defined in multiple subgraphs without proper @key directive, so the gateway treats them as separate types.
- warningField declared with different types across subgraphs (e.g., `String` vs `ID`), causing type mismatch error.
- warningMissing @external directive on fields that are resolved by another subgraph, leading to 'field not found' errors.
- warningStale gateway cache: the gateway still holds an old composed schema after subgraph updates.
- warningCustom directive defined in one subgraph but not imported/declared in the gateway configuration.
- warningVersion mismatch between Apollo Federation packages (e.g., @apollo/gateway v2 vs subgraphs using federation v1 directives)
Concrete fix directions. Pick the one that matches your root cause.
- buildAdd @key(fields: "id") to the shared type in all subgraphs that define it, and add @external to fields provided by other subgraphs.
- buildAlign field types across subgraphs — ensure the same field has the same type (e.g., always `ID` for identifiers).
- buildAdd @external to any field that is declared in a subgraph but resolved by another (the subgraph only provides the field definition for the gateway to stitch).
- buildRestart the gateway and clear its schema cache — if using in-memory cache, simply restart; if Redis, delete the key (e.g., `DEL gateway:schema`).
- buildDeclare custom directives in the gateway's supergraph config under `include_directives` or equivalent.
- buildUpgrade subgraphs to use the same federation version (e.g., migrate all to @apollo/subgraph v2) and update gateway accordingly.
A fix you cannot prove is a guess. Close the loop.
- verifiedRun `rover supergraph compose` and confirm no errors; the output should be a valid supergraph schema.
- verifiedExecute a federated query that spans multiple subgraphs (e.g., `query { order(id: "1") { user { name } } }`) and verify data returns without errors.
- verifiedCheck the gateway's health endpoint (e.g., `/health`) to confirm subgraphs are reachable and the schema is up-to-date.
- verifiedUse Apollo Studio or GraphQL Playground to introspect the gateway schema and verify the composed types and fields are correct.
- verifiedMonitor gateway logs for any composition errors after restart — should be clean for at least 10 minutes.
Things that make this bug worse or harder to find.
- warningDon't blindly restart the gateway without first inspecting the composed schema error — you'll lose the error message.
- warningAvoid duplicating type definitions across subgraphs for the sake of 'convenience' — always use @key to share entities.
- warningDon't forget to add @external when a subgraph defines a field that another subgraph resolves — this is the #1 cause of 'field not found' errors.
- warningNever ignore directive warnings during composition — they often indicate mismatched federation versions.
- warningDon't assume the gateway refreshes the schema automatically — many gateways require a restart or a cache clear after subgraph updates.
- warningAvoid defining the same custom directive in multiple subgraphs with different arguments — the gateway will fail to compose.
The Missing @key That Broke Our Federated Orders Query
Timeline
- 09:15PagerDuty alert: 500 errors on /graphql gateway endpoint — 'Type User must be defined only once'
- 09:20Check gateway logs: composition error on User type from accounts and orders subgraphs
- 09:30Fetch SDL from accounts subgraph: User has @key(fields: "id")
- 09:32Fetch SDL from orders subgraph: User has no @key, just fields id and name
- 09:35Confirm orders subgraph defines User but it's not an entity — it should extend it with @external
- 09:40Fix: In orders subgraph, change `type User` to `extend type User @key(fields: "id")` and add @external to id
- 09:45Deploy orders subgraph change to staging, run rover compose, success
- 09:50Deploy to production, restart gateway, verify query works
- 09:55Monitor: error rate drops to 0, alert resolved
At 09:15, our PagerDuty lit up with a 500 error rate spike on the GraphQL gateway. The error message was clear: 'Type User must be defined only once'. I've seen this before — it's almost always a schema composition issue in Apollo Federation. Our gateway composes schemas from three subgraphs: accounts, orders, and products. The User type is defined in accounts (it's the source of truth) and also referenced in orders. The orders subgraph had a plain `type User` without any federation directives, so the gateway saw two separate User type definitions instead of a single entity.
I quickly fetched the SDL from each subgraph using curl against the _service endpoint. Accounts had `type User @key(fields: "id")` with proper directives. Orders had `type User { id: ID! name: String! }` — no @key, no @external. That confirmed it: orders was defining User as a local type, not extending the accounts entity. The fix was to change orders to use `extend type User @key(fields: "id")` and add `@external` to the `id` field (since accounts resolves it). I also had to ensure the `name` field was either provided by orders or marked `@external` if resolved elsewhere.
After the fix, I ran `rover supergraph compose` locally and got a clean output. Deployed the updated orders subgraph to staging, verified with a federated query, then promoted to production. The gateway needed a restart to pick up the new schema — our Kubernetes deployment did a rolling restart. Within minutes, error rate dropped to zero. The lesson: always treat shared types as entities with @key in every subgraph that defines them, and use @external for fields resolved by other subgraphs. A 5-minute fix that took 30 minutes to diagnose because I didn't check the SDL first.
Root cause
Orders subgraph defined the User type without @key directive, causing the gateway to treat it as a duplicate type definition.
The fix
Changed `type User` to `extend type User @key(fields: "id")` in orders subgraph, added @external to `id` field.
The lesson
Always define shared types as entities with @key in every subgraph that references them, and use @external for fields that belong to another subgraph.
Apollo Federation composes a supergraph by merging subgraph schemas. Each subgraph declares its types, and the gateway's composition engine looks for the same type across subgraphs. If two subgraphs define the same type without federation directives, the engine throws 'Type X must be defined only once' because it doesn't know they represent the same entity. The @key directive tells the engine that a type is an entity and specifies the fields that uniquely identify it. Without @key, the engine treats the type as a value type, and duplicates are not allowed.
When a subgraph provides a field that is resolved by another subgraph, it must mark that field with @external. For example, if the accounts subgraph resolves `User.id`, the orders subgraph must declare `id: ID! @external` on its extension of User. This tells the gateway: 'I know about this field, but don't try to resolve it from me; fetch it from the source subgraph.' Missing @external is a common cause of 'Cannot query field X on type Y' errors because the gateway thinks the field exists in the subgraph but the resolver is missing.
A particularly insidious issue is when the gateway caches the composed schema and doesn't refresh it after subgraph updates. In production, we've seen cases where a developer updates a subgraph schema, deploys, and then the gateway continues to serve the old schema for minutes or hours. This leads to errors that seem to contradict the actual subgraph definitions. To diagnose, check the gateway's cache TTL (default is often 60 seconds in Apollo Gateway, but can be longer if configured). Restarting the gateway always clears the cache, but a better fix is to use a cache invalidation mechanism, such as a webhook from CI/CD to the gateway's admin endpoint.
To verify stale cache, you can query the gateway's `_service { sdl }` — it returns the composed schema. Compare that with the live subgraph schemas. If they differ, the cache is stale. Some gateways expose a `_entities` query that can also reveal if the gateway is using outdated mappings. In Kubernetes, we added a liveness probe that restarts the gateway if the schema hasn't been updated in 2 minutes, but that's a blunt instrument. A better approach is to use Apollo's managed federation or a config watcher that triggers recomposition on schema changes.
Custom directives (e.g., @auth, @rateLimit) are a frequent source of composition failures. The issue is that each subgraph defines its own directives, but the gateway must know about them to compose correctly. If a subgraph uses a custom directive that isn't declared in the gateway's configuration, the composition fails with 'Unknown directive' errors. The fix is to either declare the directive in all subgraphs consistently, or better, define it in a shared package and include it in the gateway's supergraph config.
Another pitfall: directive arguments must match exactly across subgraphs. If one subgraph defines `@auth(requires: Role)` and another defines `@auth(role: Role)`, the gateway sees two different directives with the same name, and composition fails. Always standardize directive definitions and use a shared schema file for directives. In our stack, we publish a `directives.graphql` file to an internal npm package and import it into every subgraph's type definitions.
Frequently asked questions
Why does the gateway say 'Type X must be defined only once' even though I only see it once in my code?
The error means the gateway sees the type defined in multiple subgraphs without @key. Check each subgraph's SDL (via _service endpoint) — you'll likely find the type defined in two or more subgraphs. Add @key directives to make it an entity, and use `extend type` to add fields from other subgraphs.
I added @key but still get 'Cannot query field X on type Y' — what's wrong?
This usually means a field is declared in a subgraph but not marked @external when it's resolved by another subgraph. For example, if orders subgraph declares `User.name` but accounts resolves it, you need `name: String @external` in orders. Also ensure the field's resolver exists in the source subgraph.
How do I force the gateway to refresh its schema without restarting?
Most gateways support an admin endpoint (e.g., POST /admin/schema/reload) or a mutation (e.g., `mutation { composeSchema }`). Check your gateway's documentation. Alternatively, you can delete the cache key in Redis or call the gateway's internal recompose function. Restarting is the nuclear option but always works.
Can I have the same type in multiple subgraphs without federation?
No, if two subgraphs define the same type (same name) without federation directives, the gateway will reject it. You must treat shared types as entities with @key, even if they have identical fields. The only exception is value types (like scalar types) which can be duplicated if they have exactly the same definition — but that's risky and not recommended.
Why do composition errors happen in CI but not locally?
Typically because CI uses different subgraph versions or configurations. The most common cause is that CI fetches subgraph schemas from deployed endpoints, which may be outdated or misconfigured. Run the same `rover supergraph compose` command locally with the exact same config file and subgraph URLs as CI to reproduce the error.