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 |