Skip to content
Back to Maintenance & LTS

Breaking Change Process

The end-to-end process for shipping a breaking change safely — from decision to sunset to removal.

Why

A breaking change is the most customer-impacting decision a team makes, so it needs a process that prevents both failure modes: shipping it carelessly (breaking clients) and never shipping it (the codebase rots). The process is: justify (is the break worth the cost?), version (semver major), deprecate (signal + sunset), migrate (provide a path + codemod), communicate (multi-channel), monitor (usage %), then remove (only after criteria met). Each step has a gate that prevents proceeding if the prerequisites aren't met — e.g., you can't remove until usage <1% for 30 days. This turns a scary one-off into a repeatable checklist.

When

For every breaking change. The process is the gate — no breaking change ships without passing every step.

How

1. Justify: write a 1-paragraph case for why the break is worth the migration cost (link to a decision doc)
2. Version: the break ships in a new major (semver); the old major keeps working with deprecation signals
3. Deprecate: add Deprecation + Sunset headers, @deprecated JSDoc, console.warn, changelog "Deprecated" section
4. Migrate: write docs/migrate-<old>-<new>.md with before/after + behavioral diffs + a codemod
5. Communicate: changelog + email top callers + blog post + deprecation dashboard
6. Monitor: track usage % of the deprecated thing; alert if it's not dropping
7. Remove: only after (a) sunset date passed AND (b) usage <1% for 30 days; ship the 410 Gone + Link
8. Post-mortem: did any client break? If yes, which communication channel failed — fix it for next time

Example

# BREAKING_CHANGE.md (template, filled per change)
## Change: /v1/orders response shape
### 1. Justify
v1's { items, total } wrapper is redundant; v2 returns the array directly
and puts total in a header. This simplifies 90% of client code. Worth the
migration cost for the 10% who use the wrapper.

### 2. Version
Ships in v2 (semver major). v1 keeps working with deprecation signals.

### 3-5. Deprecate + Migrate + Communicate
- Headers: Deprecation + Sunset + Link on every /v1 response
- Guide: docs/migrate-v1-v2.md (before/after + codemod)
- Email: top 100 /v1 callers, personalized with their usage count
- Blog: "Upcoming breaking change in v2" (2024-06-01)

### 6. Monitor (dashboard)
| Date       | v1 usage | v2 usage |
|------------|----------|----------|
| 2024-06-01 | 100%     | 0%       |
| 2024-09-01 | 45%      | 55%      |
| 2024-12-01 | 0.8%     | 99.2%    | <- removable!

### 7. Remove (2024-12-15)
# v1 returns 410 Gone + Link to v2
# (usage was <1% for 30 days, sunset date passed)

Common Problems

  • ⚠Skipping the justification step — a breaking change "because it's cleaner" breaks clients for an aesthetic preference.
  • ⚠Removing before the usage criteria are met — you hit the sunset date and remove, breaking the 5% still using it.
  • ⚠No post-mortem after a client breaks — the same communication gap repeats on the next breaking change.

Best Practices

  • ✓Make the process a checklist with gates — no step proceeds until the previous gate passes; this prevents shortcutting under deadline pressure.
  • ✓Require a written justification for every breaking change — if you can't articulate why it's worth the client cost, it isn't.
  • ✓Always do a post-mortem if any client breaks — the process failed somewhere, and the fix is to the process, not just this change.