Skip to main content

Command Palette

Search for a command to run...

Part 10. Migrating a Legacy Layered .NET Application to a Modular Monolith

Updated
5 min read
Part 10. Migrating a Legacy Layered .NET Application to a Modular Monolith

Most Development Teams do not wake up one morning with the freedom to design a modular monolith from scratch. They inherit something. A system that grew organically. A codebase that made sense at the time. Controllers, services, repositories, a shared DbContext, and a hundred small decisions that all seemed reasonable in isolation.

And yet, every change feels heavier than it should.

The mistake many people make at this point is assuming that architecture has to be fixed in one decisive moment. A rewrite. A migration project. A “phase two”. That thinking kills momentum, because the system doesn’t stop needing features just because you want to improve it.

A successful migration does not start with structure. It starts with pressure.

The pressure usually shows up in one place first. A part of the system that changes often. A domain that attracts bugs. A feature area that nobody enjoys touching because it breaks unrelated things.

That area is not your problem child. It’s your opportunity.

You don’t modularise the whole system. You carve out one real boundary and make it honest.

At the start, a typical layered system looks something like this.

In a typical layered system, everything flows through the same pipes. Ownership is implied rather than enforced, and data is shared by default. The architecture relies on discipline and convention instead of hard boundaries.

Trying to “add modules” on top of that usually just adds folders. The underlying model does not change. The coupling is still there, just hidden a little better. That is not the move. The first real step is to identify a business capability that can stand on its own. Not technically, but conceptually. Something you can describe clearly without mentioning how the rest of the system works. Users. Billing. Reporting. Claims. Pick one.

Then you do something that feels small but is actually profound. You stop letting other parts of the system reach into its data. You do not refactor everything at once. You draw a line, and from that point on, the rules change.

That line usually starts as a namespace and ends as an assembly. You extract the code related to that capability into a new project. Not because projects are magical, but because they give you enforcement. Suddenly, references are explicit. Internals are important. The compiler starts helping you.

At first, it will feel awkward. The rest of the system still expects to call services and repositories directly. You don’t fight that immediately. You introduce a thin contract layer and adapt.

Conceptually, the system now looks like this.

The world hasn’t changed. But one boundary is now visible.

Inside that new module, you resist the urge to replicate the layered structure. This is where vertical slices matter most. You organise by behaviour, not by technical concern. CreateUser lives in one place. GetUser lives in one place. Each slice owns its own logic end to end.

This is the moment where migration and design intersect. You’re not just moving code. You’re changing how it’s shaped.

The shared DbContext is usually the next sticking point. This is where many migrations stall, because people try to solve everything at once. You don’t.

You let the module introduce its own DbContext while the rest of the system continues using the shared one. For a while, both exist.

That coexistence is not a failure. It’s a bridge.

During this phase, the goal is not purity. It’s containment. New behaviour goes through the module’s DbContext. Old behaviour stays where it is. Over time, the shared DbContext shrinks instead of growing.

That direction matters more than speed.

Inter-module communication follows the same pattern. At first, the legacy system may still call into the module synchronously. You accept that. But inside the module, you design as if those calls were remote. Contracts are explicit. Events represent facts. Failures are handled locally. You are laying tracks in the direction you want the system to move.

One of the hardest parts of this migration is psychological, not technical. You have to be comfortable with the system being temporarily uneven. Some parts clean. Some parts messy. Some parts modern. Some parts legacy. Trying to make everything consistent too early is how migrations die.

Old code can stay ugly for a while. That’s not debt. That’s triage.

Testing often improves naturally during this process. The first module you extract becomes the first place where slice tests, integration tests, and architecture tests feel obvious instead of forced. That contrast is useful. It gives the team a reference point. People don’t need to be convinced with diagrams. They feel the difference when changes stop breaking unrelated things.

Over time, something interesting happens. The module stops feeling like “the new thing” and starts feeling normal. Meanwhile, the legacy area starts to feel increasingly uncomfortable by comparison. That discomfort is not a problem. It’s information. It tells you where to go next.

Eventually, the system’s shape changes from this.

To something more like this.

And then, slowly, Legacy gets smaller.

Not because you planned a rewrite, but because you stopped feeding it.

The most important thing to understand about this kind of migration is that it is not linear. You will pause. You will adapt. You will make compromises. That’s not failure. That’s working in a live system. The success criteria is simple. Each month, is the system easier to change than it was the month before?

If the answer is yes, you’re doing it right.

This is where the series comes full circle. A modular monolith is not a destination you reach. It’s a direction you commit to. A way of making trade-offs explicit instead of accidental.

You don’t need permission to start. You need one boundary, one module, and the discipline to protect it.

Everything else follows.


If someone reads this series end to end and only takes one thing away, I hope it’s this, architecture is not about being clever early. It’s about staying honest over time.

That’s what actually scales.