Schema Evolution: Breaking Changes, Deprecation & Migration Windows
GraphQL schemas are append-only in practice — every breaking change has a client you don't control that will silently break. The discipline of schema evolution is procedural: measure usage, communicate windows, and remove only when zero clients remain.
GraphQL Backend & API Design
Schema Evolution: Breaking Changes, Deprecation & Migration Windows
You own the schema and the resolvers — the schema is a public contract you can never silently break, and every resolver is a performance commitment you make on every query. The contract obligation means that every field you add to the schema is a commitment you must eventually manage off of — safely, verifiably, and without silently breaking any client that still depends on it. The most common GraphQL production incident is a breaking schema change that was not breaking at the SDL level: renaming a field from nullable to non-null, changing an input argument type, removing an enum value that a mobile client has cached in compiled code. These changes compile cleanly, pass your unit tests, and ship to production before you discover that 40,000 mobile clients can't render the order status screen.
This is Part 7 of the GraphQL Backend & API Design series. It connects to Part 1 (Schema Design) for the non-null and @deprecated contract, and to Part 5 (Observability) for field usage analytics as the data source that makes safe removal possible.
1. Breaking vs. Non-Breaking Change Taxonomy
Not all schema changes are equal. Understanding exactly which changes are breaking is the prerequisite for safe schema evolution.
| Change Category | Client Impact | CI Detection |
|---|---|---|
| Add optional field | None | ✅ |
| Add non-null field | Only if resolver returns null | ✅ |
| Remove field | Clients selecting it receive null (no error) | rover graph check ✅ |
| Nullable → non-null | Null propagation chain may silence data | rover graph check ✅ |
| Remove enum value | Compiled switch cases become unhandled | rover graph check ✅ |
| Change field type | Type mismatch at codegen boundary | rover graph check ✅ |
| Add required argument | Existing calls fail with validation error | rover graph check ✅ |
2. @deprecated Sunset Message Format
The @deprecated directive takes a reason string. That string is a human-and-machine-readable contract with three required pieces of information:
A complete @deprecated reason contains:
- Replacement field — the exact field name and type to migrate to
- Sunset deadline — a concrete quarter or date (not "soon")
- Migration guide — a link to the wiki, PR, or RFC where the migration is documented
3. CI-Enforced Breaking Change Detection
The gate that prevents breaking changes from reaching production is rover graph check in CI:
rover graph check compares your new schema against all registered client operation documents in Apollo GraphOS — not just against the schema itself. This is the critical distinction: nullable → non-null is only breaking if a client operation actually selects that field. If no client selects User.legacyPhone, making it non-null is safe. The check uses real operation data, not theoretical analysis.
4. The Phased Migration Playbook
Safe field removal follows a fixed five-phase process. Skipping any phase risks a production incident.
Phase 1: Add the Replacement Field
Add the new field alongside the existing one. Both are available in the schema simultaneously. Clients can migrate on their own schedule.
Phase 2: Deprecate with a Complete Reason
Phase 3: Measure Field Usage
Phase 4: Client Communication
Send a migration notice to each team owning an affected operation:
- The specific operation name and client team
- The target field and why it is better
- The sunset deadline (hard date, not a quarter)
- Offer to pair on the migration
Phase 5: Verify Zero Usage and Remove
5. Schema Versioning vs. HTTP Versioning
A common proposal when facing breaking changes is "why not just version the GraphQL API like REST?" (/v1/graphql, /v2/graphql). The answer is that schema versioning trades one problem for several worse ones:
| Dimension | Schema Evolution (additive + deprecate) | HTTP Versioning (/v2/graphql) |
|---|---|---|
| Client migration | Gradual — each client migrates independently | Hard cutover — all clients must migrate before v1 is removed |
| Schema proliferation | One schema, forward-compatible | Multiple schemas, each requiring its own resolvers and tests |
| Breaking change safety | rover graph check catches before deployment |
No automated gate — breaking changes only discovered after deployment |
| Field usage tracking | Per-field analytics from APM | No field-level visibility per version |
| Apollo Studio / codegen | Single schema source of truth | N schemas to maintain in tooling |
GraphQL schemas evolve like law: you can always add new law (additive changes), you can mark old law as obsolete (deprecation), but you cannot repeal law that people are still relying on without a formal sunset process. The additive-only discipline is not a technical constraint — it is a contract management discipline.

Adding optional fields is always safe. Removing fields, changing types, and adding required arguments are breaking changes detectable by rover graph check before deployment.

The five-phase migration playbook: add replacement field → deprecate with sunset deadline → measure usage → contact affected teams → confirm zero usage → remove. No phase is optional.
Summary
| Concept | Rule |
|---|---|
| Schema is append-only | Every field removal or type change is a breaking change in practice. The only safe evolution path is additive: add new fields, deprecate old ones, and remove only after confirmed zero usage. |
@deprecated reason format |
Must contain: replacement field name, sunset deadline (hard date), and migration guide link. Vague reasons ("use the new field") are not actionable for client teams. |
| CI gate | rover graph check runs in under 10 seconds and compares against real registered client operation documents — not just theoretical schema analysis. Make it a required CI check. |
| Phased removal | Never remove a deprecated field without: (1) confirmed zero usage from GraphOS field insights, (2) a successful rover graph check with no affected operations, and (3) 24h error monitoring post-removal. |
| No HTTP versioning | Schema versioning trades gradual migration for hard cutovers and multiplied resolver/test maintenance. The additive-only discipline is always the lower total cost. |
What's Next
Part 8 — Testing GraphQL APIs: Resolvers, Integration & Schema Contracts closes the series by covering the testing pyramid: resolver unit tests with mocked context, integration tests via graphql() execute (no HTTP), schema contract tests with client operation documents, and CI validation with rover graph check.
This article was developed with AI-assisted deep search, specification cross-referencing, and technical research synthesis.