Migrating a Legacy Codebase Incrementally Without Breaking Everything
The temptation is always the same: "This codebase is terrible. Let us rewrite it from scratch." I have seen this movie play out multiple times. The rewrite takes twice as long as estimated. It misses edge cases that the legacy code handles (but nobody remembers why). The team burns out maintaining two systems simultaneously. And sometimes the rewrite is abandoned, leaving the legacy code even more neglected.
Incremental migration is slower month-by-month but faster overall because: the system stays running throughout, you learn from each step, and you can stop at any point with a better system than you started with.
The Strangler Fig Pattern
Named after a plant that grows around a tree and eventually replaces it: build new functionality alongside the old system, gradually routing traffic and features to the new code, until the old code can be safely removed.
The key insight: you never have a "migration week" or "rewrite sprint." You have a continuous process where every new feature is built in the new pattern and every touched area is migrated incrementally.
Step 1: Define the Target Architecture
Before migrating anything, document where you want to end up. This does not need to be detailed implementation specs — just the high-level decisions: which framework/language for the new code, what the module structure looks like, which patterns to follow, and what the testing strategy is.
Without a clear target, incremental migration becomes incremental chaos — different team members migrate different areas in different directions.
Step 2: Create the Boundary
Establish a boundary between old and new code. This might be: a new directory (src/v2/ alongside src/), a new package in a monorepo, a new service behind an API, or a module boundary with defined interfaces.
The boundary must be clean: old code can call new code through defined interfaces, new code does not import old code internals, and the boundary can be tested independently.
Step 3: New Features in New Code
From this point forward: every new feature is built using the new architecture and patterns. No exceptions. This is the most important rule because it means: the old codebase stops growing, the new codebase accumulates value with every sprint, and the team builds familiarity with the new patterns.
If a new feature requires touching legacy code: build the feature in the new code and create a thin adapter that connects it to the legacy system.
Step 4: Migrate on Touch
When you need to modify legacy code (bug fix, enhancement, dependency update): migrate that specific area to the new architecture. Not the entire related module — just the touched area.
This principle means: areas that are frequently modified migrate quickly (they get touched often), areas that are stable migrate slowly or never (they do not need attention), and the migration prioritizes itself based on actual development activity.
Step 5: The Adapter Layer
During migration, old and new code coexist. They need to communicate. Build adapter layers that: translate data formats between old and new representations, route requests to the correct implementation, and maintain backwards compatibility during the transition.
The adapter is temporary infrastructure. It adds complexity but enables the migration to proceed without breaking the system. Remove adapter code as soon as both sides are migrated.
Common Migration Scenarios
JavaScript to TypeScript: start with tsconfig allowing implicit any. Rename files from .js to .ts one at a time. Add types gradually — strict where new, loose where legacy. Over months, progressively tighten the TypeScript configuration.
Class components to hooks: new components use hooks. When touching a class component for a bug fix, convert it. Do not batch-convert — each conversion is a small, reviewable PR.
REST to GraphQL: new features expose GraphQL endpoints. Old features keep REST endpoints. Clients consume both during transition. Eventually remove REST endpoints when all consumers migrate.
Monolith to microservices: extract one bounded context at a time into a service. The monolith calls the new service through an API. Other monolith features are untouched. Extract the next context when ready.
What NOT to Do
Do not run old and new in parallel expecting to "switch over": parallel running doubles your maintenance burden and usually diverges as teams prioritize one over the other.
Do not migrate bottom-up (infrastructure first, UI last): users see no improvement for months. Migrate top-down — the user-facing layer first — so improvements are visible early.
Do not try to make the migration "invisible" to the team: migration should be an explicit goal with allocated time. Pretending it happens for free leads to cutting corners.
Do not migrate everything: some legacy code works fine and is not touched. Leave it alone. Migration effort should be proportional to maintenance pain.
Measuring Progress
Track: percentage of code in new patterns vs legacy (lines of code, number of files, or component count), frequency of touching legacy code (decreasing over time indicates successful migration), build and test times (improving as legacy complexity is replaced), and developer velocity (are features faster to build?).
Do not set a "migration complete" deadline. The migration is never truly done — it transitions from active migration to maintenance mode as the legacy percentage becomes negligible.
The Team Dynamics
Migration is a team effort. Challenges: some developers love the new patterns and want to migrate everything immediately (reign them in — too fast breaks things), some developers are comfortable with the legacy and resist change (include them in new-pattern design decisions so they feel ownership), and leadership wants a date for "done" (educate on incremental value delivery).
The migration succeeds when: every team member can work in the new patterns comfortably, the legacy code shrinks with every sprint, and the team velocity improves measurably quarter-over-quarter.
Written by AIDToolStack Editorial
The AIDToolStack editorial team reviews developer tools, AI products, and MCP servers with hands-on testing and honest assessments. We use the tools we review every day in production.
References & Further Reading
Related Posts
Sponsor Our Newsletter
Reach thousands of developers who are actively evaluating AI tools, MCP servers, and dev infrastructure. Our weekly newsletter goes to engaged technical decision-makers.
All sponsored content is clearly labeled per our editorial policy.