Craft of code
Refactoring and legacy code
Change code you did not write without breaking it. Start here for characterization, seams, tiny steps, and incremental migrations.
Legacy code is code without tests you trust. Every edit is a bet until you pin what it does today. Refactoring improves structure without changing observable behavior, so the next behavior change is safe.
Big-bang rewrites usually fail. They freeze features for months, rediscover the same edge cases, and ask the business to wait. Prefer incremental improvement that keeps shipping.
Concept
Refactoring means a behavior-preserving transformation. If users can tell you changed output, you changed behavior. Martin Fowler's discipline: many tiny steps, always green.
Why this is a craft, not a cleanup week
You will spend more time changing code you did not write than writing greenfield. The skill is risk management: lock behavior, open a seam, take tiny green steps, then migrate behind a flag. Dedicated "cleanup weeks" fail when they lack a forcing product need. Opportunistic tidying on the path of real work, plus a scheduled migration when the platform is on fire, is the durable mix.
SDE2s leave the module on the critical path better than they found it. Multi-quarter stranglers need a plan, stakeholders, and a dashboard — the same ownership you would bring to a feature launch.
A rewrite is rarely the first move. It can win when the module is a prototype with no users yet, smaller than the test harness would be, or under a hard technology mandate with isolation already in place. Even then, sit it behind a facade and parallel-run. If you cannot characterize today's behavior or route gradually, you are not ready to rewrite.
In interviews, describe a strangler or expand/contract you drove. Emphasize characterization tests and separated PRs. Be honest about a rewrite you would not do again. Judgment matters.
Concepts in this section
Read them in order. Each one builds on the one before.
| Step | Concept | What it answers |
|---|---|---|
| 1 | Characterization tests | How do you lock today's behavior before you touch it? |
| 2 | Seams and dependency breaking | How do you test code that constructs everything with new? |
| 3 | Tiny refactor steps | How do you change structure without mixing in behavior? |
| 4 | Incremental migrations | How do you replace a system without a rewrite? |
| 5 | Selling tech debt | How do you fund and track the work? |
Common mistakes
- Refactoring without tests. "I am careful" is not a strategy.
- Behavior changes hidden inside cleanup PRs. Reviewers miss them; bisect lies.
- Boiling the ocean. Pick the module on the path of the next feature.
- Rewrites with no seams and no gradual cutover. Big-bang day is how you learn Feathers the hard way.
Key takeaways
- Legacy means untested. Start by pinning today's behavior.
- Open a seam, then tidy in tiny always-green steps, then change behavior in a different PR.
- Prefer strangler, expand/contract, parallel run, flags, and codemods over rewrites.
- Sell the work in incident, speed, and risk language. Stop when the next feature is easy.
Further learning
Fowler's "Workflows of Refactoring" talk names litter-pickup, comprehension, and preparatory refactoring.
Related
Writing code others can maintain
Managing complexity with deep modules, clear naming, honest comments, consistent style, and error strategies that shrink the blast radius of change.
Testing strategy
What to test, at which level, and how to keep a suite fast, reliable, and worth the maintenance — from pyramid trade-offs to production safety nets.
Monoliths and microservices
When to split a system, how to draw service boundaries, and the operational cost that microservices add.