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.