Breaking Refactors — Freedom Through Structure
Compatibility layers are coupling debt with interest. Every compat shim is a decision deferred, not a decision avoided. Break cleanly, break once, break with evidence.
Principles
- No half-measures. A partial migration is worse than no migration — it doubles the surface area and confuses every reader.
- One migration direction. Old-to-new only. Never add new-to-old adapters; that entrenches the old path.
- Blast radius awareness. Map every consumer before removing anything. Surprise breakage is a planning failure, not a courage signal.
- Dead code is a lie. "Just in case" code is not dead — it actively misleads readers about what the system does.
- Compat shims are temporary. If a shim has no removal date, it is permanent. If it is permanent, it is architecture. Decide which.
Reconnaissance Checklist
Before breaking anything, find and catalog:
@deprecated / #[deprecated] / warnings.warn markers — especially ones without removal versions
- Version-gated code paths (
if version >= X, feature flags, #[cfg(feature = "legacy")])
- Adapter / shim / bridge / wrapper layers that translate between old and new interfaces
- Dual serialization formats (v1/v2 JSON schemas, protobuf
oneof with legacy fields)
- Tests that exist solely to validate backward-compatible behavior
- Configuration keys that toggle between old and new behavior
- Re-export / forwarding modules that alias old paths to new locations
- Changelog entries promising deprecation timelines
Decision: Break or Not?
| Signal |
Break |
Do NOT Break |
| Zero external consumers |
Yes |
— |
| Single internal consumer, you own it |
Yes |
— |
| Well-tested, high coverage |
Yes |
— |
| Clear new path exists |
Yes |
— |
| External/public API with unknown consumers |
— |
Not without migration plan |
| No tests covering the boundary |
— |
Write tests first, then break |
| Multiple consumers, unclear ownership |
— |
Map consumers first |
| Compat layer under active use by migration-in-progress |
— |
Finish migration first |
Execution Strategy
- Map blast radius. List every file, module, and external consumer that references the old API. Use
ast-grep, rg, or equivalent — not guesswork.
- Snapshot current behavior. Ensure tests cover the old path. If coverage is missing, add characterization tests before removal.
- Remove the old path. Delete the compat layer, adapter, or legacy code. Do not comment it out.
- Update all call sites. Migrate every reference found in step 1 to the new API. Compile/typecheck after each batch.
- Delete orphaned tests. Tests that validated the old path are now dead weight. Remove them.
- Search for ghosts. Grep for string references, config keys, environment variables, documentation links, and error messages that mention the old API.
- Verify no dead imports/deps. Check for unused imports, packages, or dependencies that only the old path required.
Anti-patterns
- "Just in case" paths — keeping old code "in case someone needs it." That is what version control is for.
- Partial migration — half the codebase on new API, half on old. Worse than either alone.
- Commenting out instead of deleting — commented code is invisible debt that greps cannot find.
- Compat-of-compat — wrapping a compat layer in another compat layer. Two wrongs do not make an abstraction.
- Deprecated without removal date — a deprecation warning without a deadline is a suggestion, not a plan.
- Stale feature flags — flags that are always on (or always off) in every environment. Delete the flag, keep the winning path.
Validation Gates
| Gate |
Condition |
| Blast radius mapped |
Every consumer of old API identified and listed |
| Tests green pre-removal |
Existing tests pass before any deletion begins |
| Zero references post-removal |
ast-grep / rg for old API names returns zero hits |
| No dead imports/deps |
No unused imports, packages, or type declarations remain |
Exit Codes
| Code |
Meaning |
| 0 |
Clean break — old API fully removed, all consumers migrated, tests pass |
| 1 |
Partial — old references remain in code, docs, or config |
| 2 |
Tests broken — removal caused test failures not yet resolved |
| 3 |
External consumers found — need migration plan before proceeding |
1---2name: refactor-break-bw-compat3description: Refactor by removing backward compatibility and legacy layers. Use when modernizing APIs, cleaning up migration debt, removing compat shims, or eliminating stale feature flags.4---5
6# Breaking Refactors — Freedom Through Structure
7
8Compatibility layers are coupling debt with interest. Every compat shim is a decision deferred, not a decision avoided. Break cleanly, break once, break with evidence.
9
10## Principles
11
121. **No half-measures.** A partial migration is worse than no migration — it doubles the surface area and confuses every reader.
132. **One migration direction.** Old-to-new only. Never add new-to-old adapters; that entrenches the old path.
143. **Blast radius awareness.** Map every consumer before removing anything. Surprise breakage is a planning failure, not a courage signal.
154. **Dead code is a lie.** "Just in case" code is not dead — it actively misleads readers about what the system does.
165. **Compat shims are temporary.** If a shim has no removal date, it is permanent. If it is permanent, it is architecture. Decide which.
17
18## Reconnaissance Checklist
19
20Before breaking anything, find and catalog:
21
22- `@deprecated` / `#[deprecated]` / `warnings.warn` markers — especially ones without removal versions
23- Version-gated code paths (`if version >= X`, feature flags, `#[cfg(feature = "legacy")]`)
24- Adapter / shim / bridge / wrapper layers that translate between old and new interfaces
25- Dual serialization formats (v1/v2 JSON schemas, protobuf `oneof` with legacy fields)
26- Tests that exist solely to validate backward-compatible behavior
27- Configuration keys that toggle between old and new behavior
28- Re-export / forwarding modules that alias old paths to new locations
29- Changelog entries promising deprecation timelines
30
31## Decision: Break or Not?
32
33| Signal | Break | Do NOT Break |
34|--------|-------|--------------|
35| Zero external consumers | Yes | — |
36| Single internal consumer, you own it | Yes | — |
37| Well-tested, high coverage | Yes | — |
38| Clear new path exists | Yes | — |
39| External/public API with unknown consumers | — | Not without migration plan |
40| No tests covering the boundary | — | Write tests first, then break |
41| Multiple consumers, unclear ownership | — | Map consumers first |
42| Compat layer under active use by migration-in-progress | — | Finish migration first |
43
44## Execution Strategy
45
461. **Map blast radius.** List every file, module, and external consumer that references the old API. Use `ast-grep`, `rg`, or equivalent — not guesswork.
472. **Snapshot current behavior.** Ensure tests cover the old path. If coverage is missing, add characterization tests before removal.
483. **Remove the old path.** Delete the compat layer, adapter, or legacy code. Do not comment it out.
494. **Update all call sites.** Migrate every reference found in step 1 to the new API. Compile/typecheck after each batch.
505. **Delete orphaned tests.** Tests that validated the old path are now dead weight. Remove them.
516. **Search for ghosts.** Grep for string references, config keys, environment variables, documentation links, and error messages that mention the old API.
527. **Verify no dead imports/deps.** Check for unused imports, packages, or dependencies that only the old path required.
53
54## Anti-patterns
55
56- **"Just in case" paths** — keeping old code "in case someone needs it." That is what version control is for.
57- **Partial migration** — half the codebase on new API, half on old. Worse than either alone.
58- **Commenting out instead of deleting** — commented code is invisible debt that greps cannot find.
59- **Compat-of-compat** — wrapping a compat layer in another compat layer. Two wrongs do not make an abstraction.
60- **Deprecated without removal date** — a deprecation warning without a deadline is a suggestion, not a plan.
61- **Stale feature flags** — flags that are always on (or always off) in every environment. Delete the flag, keep the winning path.
62
63## Validation Gates
64
65| Gate | Condition |
66|------|-----------|
67| Blast radius mapped | Every consumer of old API identified and listed |
68| Tests green pre-removal | Existing tests pass before any deletion begins |
69| Zero references post-removal | `ast-grep` / `rg` for old API names returns zero hits |
70| No dead imports/deps | No unused imports, packages, or type declarations remain |
71
72## Exit Codes
73
74| Code | Meaning |
75|------|---------|
76| 0 | Clean break — old API fully removed, all consumers migrated, tests pass |
77| 1 | Partial — old references remain in code, docs, or config |
78| 2 | Tests broken — removal caused test failures not yet resolved |
79| 3 | External consumers found — need migration plan before proceeding |