Refactoring Patterns: Strangler Fig, Extract Class, Anti-Corruption Layer & Legacy Modernization
The production refactoring toolkit for senior engineers: how to safely modernize legacy codebases without a big-bang rewrite. Covers Strangler Fig migration, Extract Class/Interface, Introduce Parameter Object, Anti-Corruption Layer, and the Branch-by-Abstraction technique with concrete Java/Python examples.
Why Refactoring Patterns Are an Interview Topic
Staff and senior engineering interviews increasingly include "production engineering" questions: "How would you modernize this legacy codebase without downtime?" or "The checkout service is a 15,000-line class. How do you break it up?" These questions test whether you can change a running system safely — a skill that's more valuable in production than any greenfield architecture.
The core challenge: you can't stop the world while you refactor. The system must keep working during the migration. This requires patterns that allow the old and new code to coexist temporarily.
What Earns Each Level on Refactoring Questions
6/10: "We'd rewrite it from scratch" or "extract it into a separate service." No migration plan, no risk management.
8/10: Describes the strangler fig pattern. Mentions feature flags. Knows Extract Class and Extract Method from Fowler's Refactoring book.
10/10: Designs the full migration plan with a dual-write period, anti-corruption layer between old and new models, branch-by-abstraction to keep the team shipping while the refactor is in flight, and explicit rollback triggers. Talks about test coverage as a prerequisite ("you can't safely refactor code that has no tests").
The Core Refactoring Patterns
Strangler Fig — Migrate Without Big-Bang Rewrite
Route traffic to the new implementation via a facade/proxy. Old system handles everything it currently handles; new system handles newly routed paths. Incrementally move traffic until the old system is strangled. Safe because: rollback = revert routing. Used by: Netflix, Shopify, GitHub for monolith migrations. Example: add an API gateway in front of a monolith, route /payments to the new Payment service, everything else to monolith.
Branch by Abstraction — Keep Shipping During Refactor
Introduce an interface/abstract class in front of the code you want to change. Production uses the old implementation; new implementation is behind the interface. Swap the implementation when the new one is ready. Teams can merge to main daily because the abstraction prevents compile-time breakage. The interface is the seam — change what's behind it without affecting callers.
Extract Class — Break the God Class
A class with 20 responsibilities has 20 reasons to change (violates SRP). Identify a cohesive subset of fields and methods that belong together. Create a new class. Move the fields (update all references). Move the methods (update all callers). Make the old class delegate to the new one. Each Extract Class step should be a separate, reviewable commit. Signal: 'I can describe what this class does without using the word AND.'
Extract Interface / Introduce Interface
When a class is used in many places and you need to swap implementations: extract an interface from the existing class. All callers now depend on the interface, not the concrete class. Enables: mocking in tests, multiple implementations (e.g., CachingPaymentGateway wraps RealPaymentGateway via the same interface), and the Branch by Abstraction technique above.
Anti-Corruption Layer (ACL)
When integrating legacy code with a new system that uses a different domain model: create an adapter/translator layer that converts between the two models. The new system never sees the legacy model's anemic DTOs and database column names. The ACL translates them into rich domain objects. Example: legacy system has flat 'Customer' DB record with 40 fields; new system models 'Buyer', 'ShippingAddress', 'PaymentMethod' as separate objects. ACL converts between them.
Introduce Parameter Object
A method with 8 parameters is hard to call, hard to test, and hard to extend. Group logically related parameters into a value object. Before: checkout(userId, cartId, couponCode, shippingAddr, billingAddr, paymentToken, currency, express). After: checkout(CheckoutRequest) where CheckoutRequest is an immutable value object with all these fields. Easier to add new parameters (add a field), easier to mock in tests.
Strangler Fig Migration: Step-by-Step
The Dual-Write Pattern for Data Migration
The hardest part of any refactoring that changes data storage is migrating the data without downtime. The dual-write pattern handles this safely:
Phase 1 — Dual-write, old DB authoritative: Write to both old DB and new DB. Read from old DB. Old is authoritative. New DB is populated but not trusted yet. Duration: run for 1–2 weeks. Verify new DB has consistent data by running comparison queries.
Phase 2 — Dual-write, new DB authoritative: Write to both. Read from new DB. New is authoritative. Old DB still receives writes as a fallback/backup. Duration: run for 1 week. Monitor closely. Rollback = flip reads back to old DB.
Phase 3 — Single-write, new DB only: Stop writing to old DB. Old DB is frozen as a backup. Reads from new DB. Duration: keep old DB alive for 2 weeks (rollback window). Then decommission.
Consistency check during dual-write: Run a background job that reads matching records from both DBs and compares. Log any divergence. Fix the sync bug before advancing to the next phase. Never advance phases if divergence rate > 0.01%.
Pattern Selection Guide
| Problem | Pattern | Key condition to apply it | Risk if misapplied |
|---|---|---|---|
| Monolith → microservice migration | Strangler Fig | New service is independently deployable with its own DB | Missing anti-corruption layer causes both systems to share a confused domain model |
| God class (one class does everything) | Extract Class | Can identify a cohesive subset of responsibilities | Extracting without tests first causes silent regressions |
| Long parameter list (>4 params) | Introduce Parameter Object | Parameters logically belong together as a concept | Creating a parameter object that's just a bag of fields (same problem, different container) |
| Need to swap implementations without breaking callers | Extract Interface + Branch by Abstraction | Callers are injected the dependency (not instantiated inline) | Interface extracted too late — callers already have concrete type dependencies throughout the codebase |
| Integrating with legacy system with bad domain model | Anti-Corruption Layer | New system has a clean domain model worth protecting | ACL becomes too thick — re-implements business logic instead of just translating |
| Need to ship features while refactoring in progress | Branch by Abstraction + Feature Flags | New implementation passes all existing tests | Feature flag debt — forgotten flags left permanently on create dead code |
Refactoring Anti-Patterns to Avoid
1. Refactoring without tests. The #1 rule: if there are no tests covering the code you're refactoring, write characterization tests first. Run the code with various inputs, record the outputs. These become your regression suite — any refactoring that changes the output is caught immediately.
2. Big-bang refactor on a long-lived branch. A 6-week refactor branch that diverges from main creates a merge nightmare and kills team velocity. Fix: use branch-by-abstraction — the refactor lives on main, hidden behind an interface. Merge daily.
3. Semantic changes during refactoring. Refactoring means changing structure, not behavior. If you change variable names AND fix a bug in the same commit, you can't distinguish which change caused a regression. Rule: one refactoring step per commit, no behavior changes.
4. Extracting a service before the domain model is stable. If you extract a Payments microservice while the payment flow is still changing every week, you'll spend more time updating the API contract than building features. Extract when the domain is understood, not when it's still being discovered.
5. Leaving dual-write running forever. Dual-write adds write latency and consistency bugs. It's a migration tool, not an architecture. Set a hard deadline (4 weeks) and force the cutover.
Interview Delivery Summary
When asked to refactor a legacy system, start with: "First, I'd ensure test coverage of the existing behavior — you can't safely refactor code you can't verify." This signals production engineering maturity immediately.
Then describe the strangler fig for large-scale migration, branch-by-abstraction for keeping the team shipping during the refactor, and dual-write for safe data migration. Always mention rollback: "At every phase, rollback is a routing change or a feature flag flip — never a re-deploy."
The staff signal: bring up the anti-corruption layer to protect the new domain model from the legacy model's shape. This is the part most candidates miss.