Handling Enum Value Deprecation Across Subgraphs
Removing or replacing a value from an enum that several subgraphs declare is one of the easiest ways to break a federated graph: composition can silently change the published value set, or a client can crash on a value that vanished underneath it. This page covers how to deprecate enum values safely when the enum is shared — @deprecated on values, the merge rules Federation v2 applies during composition, coordinating removal across subgraphs, and migrating clients. It extends the broader Managing Shared Enums Across Subgraphs guide and the Best Practices for Shared Enums Across Federated Services page.
When to use this pattern
- A value in a shared enum is being renamed, consolidated, or retired and the enum appears in more than one subgraph.
- You need the value to keep working for existing clients during a migration window rather than disappearing in one deploy.
- The enum is used in both output positions (return types) and input positions (arguments / input fields), so removal order matters.
Prerequisites
Why enum merge rules make deprecation tricky
The hard part is that Federation v2 does not merge every shared enum the same way. The composition engine looks at how the enum is used across the supergraph and picks a merge strategy accordingly:
- Output-only enums (the enum appears only in return positions) merge by union. The supergraph value set is the union of all subgraph declarations. A value present in one subgraph but missing in another still appears in the API.
- Input-only enums (the enum appears only in argument or input-field positions) merge by intersection. The supergraph value set is the intersection — only values present in every declaring subgraph survive.
- Enums used in both positions must be declared identically in every subgraph, or composition fails.
This is the safety mechanism that keeps the contract sound: a value clients can send (input) must be understood by every subgraph that might receive it, so intersection is correct; a value clients can receive (output) is fine to expose as long as one subgraph can produce it, so union is correct.
The deprecation implication is direct: dropping a value from one subgraph’s input enum removes it from the supergraph immediately (intersection), which is a breaking change the moment that single subgraph deploys. Dropping it from an output enum has no effect until it is gone from every subgraph (union). You must know which position your enum occupies before you touch any value.
Implementation walkthrough
The safe sequence is: deprecate everywhere first, keep accepting and producing the value during the window, then remove from all subgraphs in a coordinated step. @deprecated is the signalling tool — it never changes the published value set, it only marks a value as discouraged so tooling and clients can react.
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.9", import: ["@key"])
# Used as both output (Order.status) and input (orders(filter:)), so it must
# be declared identically in every subgraph until removal completes.
enum OrderStatus {
PENDING
PROCESSING
# Deprecate in ALL subgraphs in the same release. SHIPPED is folded into DELIVERED.
SHIPPED @deprecated(reason: "Consolidated into DELIVERED. Removal: 2026-09-01.")
DELIVERED
CANCELLED
}
type Order @key(fields: "id") {
id: ID!
status: OrderStatus! # output position
}
input OrderFilter {
status: OrderStatus # input position — controls intersection merge
}
type Query {
orders(filter: OrderFilter): [Order!]!
}
While the deprecation window is open, resolvers should keep accepting the deprecated input value and stop emitting the deprecated output value, mapping it to its successor so clients see the new value before the old one is removed:
// orders-subgraph/resolvers.ts
const OUTPUT_FALLBACK: Record<string, string> = { SHIPPED: 'DELIVERED' };
// During the window, treat an incoming SHIPPED filter as DELIVERED so both behave identically.
const INPUT_NORMALIZE: Record<string, string> = { SHIPPED: 'DELIVERED' };
export const resolvers = {
Query: {
orders: async (_: unknown, { filter }: { filter?: { status?: string } }, ctx: Context) => {
const status = filter?.status ? (INPUT_NORMALIZE[filter.status] ?? filter.status) : undefined;
const rows = await ctx.db.orders.find({ status });
return rows.map(r => ({ ...r, status: OUTPUT_FALLBACK[r.status] ?? r.status }));
},
},
Order: {
// Mapping at the field level guarantees no stored SHIPPED row leaks to a client.
status: (order: { status: string }) => OUTPUT_FALLBACK[order.status] ?? order.status,
},
};
The mapping has to live in every subgraph that can return or accept the value. If one fulfilment subgraph keeps emitting SHIPPED after the orders subgraph removed it, clients hit a serialisation error on a value no longer in the supergraph — the same drift failure described in the shared enum best-practices guide.
Coordinating removal across subgraphs
Because composition merges across all declarations, removal is a fleet operation, not a per-service one. Use rover subgraph check to make the breaking change visible before it merges:
# Check the proposed removal against the registry and live operation traffic.
rover subgraph check orders \
--schema ./services/orders/schema.graphql \
--name orders
For an enum used in input or both positions, the check reports removing SHIPPED as a breaking change and, if you have operation metrics, flags any recent client that sent it. The removal release then proceeds:
- Confirm telemetry shows zero live usage of
SHIPPED(sent as input and returned as output) for the agreed quiet period. - Remove the value from every declaring subgraph in a single coordinated deploy. For a both-positions enum this is mandatory — a partial removal fails composition with mismatched declarations.
- Re-compose and publish. Verify the published supergraph SDL no longer lists the value.
- Remove the now-dead fallback mapping from resolvers in a follow-up cleanup release.
Verification steps
- After deprecating, compose and confirm the value still appears but carries the deprecation:
rover supergraph compose --config supergraph.yaml | grep -A2 "enum OrderStatus"
- Query for an order whose stored status is the deprecated value and confirm the resolver maps it forward:
query { orders(filter: { status: DELIVERED }) { id status } }
Expected — no order comes back with status: "SHIPPED"; mapped rows return DELIVERED.
- After removal, send the old value as input and confirm a clean validation error rather than partial data:
{
"errors": [
{ "message": "Value \"SHIPPED\" does not exist in \"OrderStatus\" enum." }
]
}
- Run
rover subgraph checkon the removal commit and confirm it is reported as breaking, proving the registry caught it before merge.
Common mistakes and gotchas
- Removing from one subgraph and assuming union safety. If the enum is used in any input position, the merge is intersection — dropping the value from a single subgraph deletes it from the supergraph immediately, breaking clients without warning.
- Trusting
@deprecatedto remove anything.@deprecatedonly annotates; the value stays fully valid and in the published set. Removal is a separate, coordinated step. Deprecating without later removing leaves the value live indefinitely. - Leaving the output fallback in one subgraph behind. If any subgraph still emits the removed value, clients get a serialisation error. Strip the value from production data or keep the forward-mapping until you are certain no row carries it.
Frequently Asked Questions
Does @deprecated on an enum value remove it from the supergraph?
No. @deprecated only marks the value as discouraged so tooling and clients can migrate. The value remains valid and present in the composed supergraph until you delete it from the subgraph SDLs and re-compose.
Why did removing an enum value from one subgraph immediately break clients?
The enum was used in an input position, where Federation v2 merges by intersection — only values present in every declaring subgraph survive. Dropping it from any single subgraph removes it from the supergraph at once. Output-only enums merge by union and tolerate per-subgraph differences. See Best Practices for Shared Enums Across Federated Services for the alignment rules.
How do I migrate clients off a deprecated enum value safely?
Deprecate the value in every subgraph in one release, map it forward to its successor in resolvers for both input and output, watch telemetry until live usage hits zero for an agreed window, then remove it from all subgraphs in a single coordinated deploy and re-compose.
Related
- Best Practices for Shared Enums Across Federated Services — sibling guide on enum alignment and drift detection
- Managing Shared Enums Across Subgraphs — parent guide
- Subgraph Implementation & Entity Resolution — section overview
- Implementing Entity Resolvers with @key Directives — entities that carry enum-typed fields
- Resolving Schema Conflicts in Apollo Federation — composition errors when declarations diverge