Transition from Legacy
How to introduce Clean Architecture into an existing codebase - the technical strategy, the organizational reality, and why it gets worse before it gets better.
The Starting Point
Most teams reading this are not starting a new project. They have a codebase - one that works, ships features, and makes money. It also has years of shortcuts baked in: singletons wired to everything, game logic embedded in engine objects, features tangled together so deeply that changing one system means debugging three others. The previous articles describe an architecture that sounds appealing in theory. This article is about how to get there from where you actually are.
The honest truth: this is not a weekend project. In our experience, a meaningful transition took over two years with a dedicated team. The codebase didn’t stop shipping features during that time - it couldn’t. The architecture had to be introduced incrementally, alongside ongoing development, without breaking what already worked.
The Uncomfortable Math
Before discussing any technical strategy, the organizational reality needs to be addressed - because most migration attempts fail not for technical reasons, but because the team or the organization wasn’t prepared for what it actually takes.
Development will slow down before it speeds up. Every task that touches migrated or migrating code now carries overhead - defining interfaces, building adapters, writing tests, respecting the boundary rules. A feature that would have taken three days in the old codebase might take five during the transition. This is not a sign of failure. It’s the cost of paying down debt while continuing to build.
The first few steps are the hardest. The boundary infrastructure doesn’t exist yet. There’s no DI container configured, no adapter pattern established, no conventions for how old and new code coexist. Each early step creates infrastructure that every subsequent one reuses. The tenth feature migrated is dramatically cheaper than the first.
The timeline is measured in months, not weeks. In our experience, visible benefits - faster feature development in clean areas, bugs caught by tests, easier onboarding - started appearing after roughly six months of consistent effort. The full transition took over two years. Every team’s timeline will differ based on codebase size, team size, and how much of the codebase is actively changing.
The alternative is worse. The codebase is already slowing down. Features already take longer with each release. Bugs already cascade unpredictably. The question isn’t whether you can afford to migrate - it’s whether you can afford not to. The debt compounds either way; the only choice is whether to start paying it down.
The Champion
Architectural migration doesn’t happen by committee. It needs a champion - a person (or a small, committed group) who owns the vision and drives it forward.
The champion’s role:
- Guards the boundary. Reviews every PR that touches the border between old and new code. Ensures clean code never references legacy systems directly. Pushes back when someone wants to “just this once” take a shortcut.
- Builds the infrastructure. Sets up the DI composition root, establishes adapter patterns, creates the first vertical slice as a reference implementation. Makes it easy for others to do the right thing.
- Absorbs the doubt. When a developer says “this is taking twice as long,” the champion acknowledges it, explains why, and shows what it’s building toward. When management questions the investment, the champion has concrete metrics - test coverage, bug rates in new vs. old code, time-to-implement for new features.
- Celebrates wins. The first test that catches a regression. The first feature that shipped in half the expected time because it was built cleanly. The first time a new team member was productive in days instead of weeks. And eventually, removing the last singleton from the codebase - that one feels like defeating the final boss. These moments matter - they build the institutional belief that the investment is paying off.
The champion doesn’t need to be the most senior engineer. They need conviction, patience, and the ability to maintain standards without becoming adversarial. In our experience, having someone fully invested in the architecture - who sees it not as extra work but as the way work should be done - made the difference between a gradual migration and an abandoned experiment.
Getting Developer Buy-In
The architecture is only as good as the team’s willingness to follow it. Developers who feel forced into a pattern they don’t understand will find ways around it.
Frame it as an investment, not a criticism. The existing codebase was built under real constraints - deadlines, changing requirements, limited knowledge. Acknowledging that context matters. The migration isn’t about “fixing bad code” - it’s about giving the team better tools to work with going forward.
Make the right thing easy. If following the architecture is harder than not following it, people won’t follow it. Provide templates for new features. Document the vertical slice structure with a reference implementation. Set up architectural tests that catch boundary violations automatically - developers shouldn’t need to memorize the rules.
Show, don’t argue. The most effective conversion tool is a side-by-side comparison: “Here’s what adding a feature looks like in the old code. Here’s what it looks like in clean code.” When a developer experiences the difference firsthand - writing a test in milliseconds instead of launching the game, changing a system without breaking three others - the argument makes itself.
Start with volunteers. Don’t mandate adoption across the entire team on day one. Find the developers who are curious or frustrated enough to try the new approach. Let them build the first few slices. Their experience and advocacy will be more convincing than any architecture document.
Getting Management Buy-In
Management cares about shipping. The architectural argument needs to be framed in those terms.
Quantify the current cost. How long does a “simple” feature take? How many bugs are introduced per release? How long does onboarding take? How much time is spent on regression testing? These numbers make the case that the status quo is already expensive.
Set expectations honestly. Don’t promise that migration will be painless. Say: “For the next three to six months, feature work will be roughly 20-30% slower. After that, we expect to see parity, and within a year, we expect to be measurably faster.” The exact numbers depend on your situation, but the shape of the curve is consistent - short-term cost, long-term gain.
Make progress visible. Track the ratio of clean to legacy code. Show the number of tests. Show the time-to-implement for features in new vs. old code. Management doesn’t need to understand the architecture - they need to see that the investment is producing measurable results.
Signs It’s Working
The benefits compound gradually, then suddenly. Here’s what to watch for:
- New features are faster to build. Not the first few - those carry migration overhead. But once the infrastructure exists, building in clean code becomes noticeably faster than building in the old codebase.
- Bugs are easier to locate. When something breaks in clean code, the layered structure and test coverage narrow the search immediately. “The test for CompleteQuestUseCase is failing” is a very different starting point than “something is wrong with quests.”
- Tests catch regressions. The first time a test catches a bug that would have shipped to players, the team’s relationship with testing changes permanently.
- Onboarding accelerates. New developers can understand a clean vertical slice in hours. The folder structure tells them what the feature does, the interfaces tell them how it connects, and the tests tell them how it behaves.
- Refactoring becomes safe. Changing a system’s internals without changing its interfaces - and having tests verify that behavior is preserved - is transformative for a team accustomed to “don’t touch it, it works.”
None of these benefits appear on day one. Most appear after several months of consistent effort. All of them compound over time. The codebase doesn’t just get better - it gets easier to make better, which is the real return on investment.
We went through this ourselves. The decision to restructure came during a difficult period for the studio, and the early months were rough - every feature carried migration weight, and there were genuine moments of doubt from developers, management, and ourselves. But eventually the curve flipped. New features started landing faster than they ever had in the old codebase. The benefits became obvious to everyone, not just the architects. Looking back, that transformation was the single biggest reason the studio came out of that crisis stronger than before.
The Two Zones
With the organizational foundation in place, here’s the technical strategy. The core idea is to split the codebase into two explicit zones: Legacy and Clean.
Project/
+-- Legacy/ <- everything that exists today
� +-- QuestManager.cs
� +-- InventoryUI.cs
� +-- GameManager.cs
� +-- SaveSystem.cs
� +-- ...hundreds of files
�
+-- Clean/ <- new architecture lives here
+-- Features/
+-- (empty, for now)
On day one, you move the entire existing codebase into Legacy/. Nothing changes functionally - the game still compiles and runs exactly as before. What changes is the signal: everything in Legacy/ is acknowledged technical debt. Everything in Clean/ follows the architectural rules.
The Boundary Rules
The relationship between the two zones is strict and asymmetric:

- Clean never references Legacy. Not through a helper class, not through a “temporary” shortcut, not through a static accessor. If Clean code needs something that currently lives in Legacy, that something must be extracted, wrapped in an interface, or rebuilt.
- Legacy can reference Clean - but only through public interfaces. When a Legacy system needs to call into a Clean feature, it does so through the feature’s Use Case interface. It never reaches into the feature’s Core or internal Use Case implementations. This is the same dependency rule that governs the layers, now applied at the codebase level.
- New code goes into Clean. This is the hardest rule to enforce, and the most important. Every new feature, every significant change, should land in Clean. This often means that a task that would take two days in Legacy takes four - because you’re also extracting the interfaces and dependencies needed to do it cleanly. This overhead is the cost of migration, and it must be accepted upfront.
One Slice at a Time
The migration unit is a vertical slice. You don’t migrate “all the entities” or “all the controllers.” You migrate one feature - its Core, its Use Cases, its Logic, its Components - as a complete unit.
Picking the First Slice
The first slice matters disproportionately. It’s the proof of concept that either builds momentum or becomes evidence that “this doesn’t work.” Pick a feature that:
- Is about to change anyway. You’re already going to touch it - the migration overhead is partially absorbed by the planned work.
- Has clear boundaries. A self-contained system with identifiable inputs and outputs. Quest completion, inventory management, a settings screen - not “the core game loop.”
- Is representative but not critical-path. Complex enough to demonstrate the architecture’s value, but not so central that a migration failure blocks the entire team.
- Interacts with other systems. A feature that only talks to itself doesn’t prove much. You want at least one cross-feature boundary to demonstrate how Legacy and Clean coexist.
The Extraction Process
Migrating a slice typically follows this sequence:
-
Identify the domain. What are the entities, the rules, the state? Extract these into the feature’s Core. They should have zero dependencies on Legacy code or engine types.
-
Define the Use Case interfaces. What does this feature offer to the rest of the system? This is the feature’s public API - the only thing Legacy code will be allowed to call.
-
Implement the Use Cases. Build the use case implementations. This is where you’ll feel the most friction - the Use Cases layer needs interfaces for things that currently live in Legacy as concrete classes. You’ll need to create adapter interfaces that wrap legacy systems behind clean contracts.
-
Build or migrate the Controllers and Views. The UI and presentation layer. If the existing UI is too entangled to migrate cleanly, it’s sometimes easier to rebuild it against the new clean interfaces than to untangle the old one.
-
Wire Legacy to Clean. Update the Legacy code to call the new feature through its Use Case interfaces. Remove the old implementation. The feature now lives in Clean, and Legacy depends on it through the proper boundary.
The most common friction point throughout this process is dependencies. Your clean feature needs to persist data, but the save system lives in Legacy. Your clean feature needs player state, but it’s buried in a god-object GameManager. The solution is always the same: define an interface in Clean that describes what you need, then implement an adapter in Legacy that wraps the old system behind that interface. When the old system is eventually migrated, the adapter disappears - and the feature that depended on it doesn’t change at all.
What Stays Legacy
Not everything needs to migrate. Some systems are:
- Stable and rarely touched. If the audio manager hasn’t changed in two years and works fine, leave it in Legacy behind an interface. The boundary protects you.
- Scheduled for replacement. If you’re going to replace the networking stack next quarter anyway, don’t invest in migrating the current one. Wrap it in an adapter and build the replacement in Clean.
- Too entangled to justify the cost. Some legacy systems are so deeply wired into everything that extracting them would require migrating half the codebase first. Contain them behind interfaces and migrate their dependents first - eventually the entangled system becomes isolated enough to extract.
The goal isn’t zero Legacy. The goal is that new work happens in Clean, that Clean is growing while Legacy is shrinking, and that the boundary is respected. A codebase that is 60% Clean and 40% contained Legacy is in a dramatically better position than one that is 100% tangled.