Practical

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:

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:

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:

Legacy zone calls into Clean zone through Use Case interfaces only - Clean never references Legacy

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:

The Extraction Process

Migrating a slice typically follows this sequence:

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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:

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.