18.4 Legacy code: extract seams before changing behavior
Overview and links for this section of the guide.
On this page
Why legacy code is risky
Legacy code is risky because:
- behavior is often implicit and undocumented,
- tests may be missing or low coverage,
- side effects and coupling are common,
- refactors can trigger subtle regressions.
Vibe coding can accelerate changes, but without guardrails it can accelerate breakage too.
Rewrites erase history, break contracts, and are hard to review. The safe move is to extract seams and change behavior behind tests.
What “extract seams” means
A seam is a boundary you create that lets you change behavior safely:
- a wrapper around a dependency,
- an interface you can mock,
- a function extraction that isolates logic,
- a module split that reduces coupling.
Seams let you test and modify parts of the system without touching everything at once.
A safe legacy workflow
- Reproduce: identify the behavior to preserve/change.
- Characterize: write tests that lock current behavior (even if it’s weird).
- Extract seam: introduce a boundary with minimal behavior change.
- Refactor behind seam: improve internals while tests stay green.
- Change behavior: update tests and implementation intentionally.
This is how you make progress without gambling.
Characterization tests (how to freeze behavior)
Characterization tests are tests that describe current behavior rather than desired behavior. They are useful when you don’t fully trust your understanding.
Practical approach:
- pick a representative input and capture output
- write a test that asserts the current output
- repeat for a few edge cases
Then refactor confidently: if behavior changes, tests catch it.
Even if the behavior is wrong, lock it first. Then change it intentionally with new acceptance criteria.
Practical seam extraction techniques
- Wrap external calls: create a single module for API/DB access.
- Dependency injection: pass dependencies as parameters so tests can inject fakes.
- Extract pure functions: move logic out of side-effecting code into testable functions.
- Introduce adapters: convert messy inputs into clean internal types at boundaries.
These are boring techniques. They are also how legacy systems become maintainable.
Copy-paste prompts for legacy work
Prompt A: write characterization tests only
We are working with legacy code.
Task:
Write characterization tests that capture current behavior for these cases:
- Case 1: ...
- Case 2: ...
Constraints:
- Do not change implementation yet
- Keep tests deterministic
- Output diff-only changes (tests only)
Prompt B: extract a seam with minimal behavior change
Extract a seam so we can refactor safely.
Constraints:
- Preserve behavior (tests must stay green)
- Diff-only changes
- Keep scope limited to these files: [...]
Output:
1) short plan
2) diff-only patch for step 1