Modular monoliths spent two years on the conference circuit. Now that the noise has died down, what does it actually look like to build one in .NET 9? Here is what we are doing, why, and where the seams need to be.

The case, briefly

A modular monolith is one deployable that behaves internally like many. Bounded contexts with their own data, their own domain model, and their own service surface — but compiled together, shipped together, and run as a single process. You get most of the operational simplicity of a monolith and most of the design clarity of microservices.

You also get the work of drawing the seams. That is the part the conference talks skipped.

Where the seams go

Three boundaries matter more than the rest. Get these right and the rest follows. Get them wrong and the monolith earns its bad reputation.

1. The data boundary.

Every module owns its tables. Other modules read the data through the module's service surface, not by joining across schemas. This is the boundary that decays first if you do not defend it. We use a separate schema per module and a periodic check in CI that flags cross-schema queries.

2. The domain boundary.

No shared entity types across modules. The Customer in the billing module is a different Customer to the one in the support module. They probably overlap; they may even share an id. They do not share a class. Allowing the sharing is how a modular monolith becomes a regular one.

3. The communication boundary.

Modules talk to each other through a small set of interfaces, ideally over an in-process message bus rather than direct method calls. We use MediatR for this in our .NET work — it makes the call surface explicit and the dependencies visible. The alternative is a forest of injected interfaces that look fine until you try to extract a module.

The rule: a modular monolith is a microservices architecture you have decided not to deploy yet.

The .NET 9 specifics

A few .NET 9 choices that paid off:

  • Folder-per-module. Each module lives in its own folder under /src/Modules/<Name>/ with its own .csproj. The composition root is a tiny project that references all of them.
  • Module registration. Each module exposes a static AddModule() extension that registers its services into the DI container. The composition root calls each one. No reflection, no magic.
  • EF Core per module. Each module has its own DbContext, its own migrations folder, its own schema. No shared DbContext.
  • MediatR for cross-module calls. A request handler in one module can call a notification or query in another via MediatR. The call surface stays narrow.

What this is not

It is not a microservices architecture in waiting. Building a modular monolith with the intent to "split it later" is usually a mistake — you end up with neither the design clarity of micro-services nor the operational simplicity of a monolith, because you over-engineered for the split.

Build the modular monolith because the modularity is the design value. If you ever need to split it, the seams are already in the right place. But ship the monolith as if you will be running it for the next ten years. Often you will be.

Where it falls down

The shared deployment is the limit. If one module needs to scale independently — say, a high-throughput ingestion path — the modular monolith is the wrong choice. The same is true if different modules have very different uptime requirements or compliance scopes. In those cases, deploy them separately and pay the operational cost.

The other failure mode is social, not technical. A monolith with eight teams sharing it is a coordination problem dressed as an architecture. Modular boundaries do not fix that — they just expose it.

Closing

A modular monolith is a piece of self-restraint as much as it is a technical pattern. You are saying: we are not going to ship eight services to solve a one-team problem. We are going to draw the seams properly and ship one process. If, later, we want to split it, the work is already done.

That restraint is the value. The technical pattern is the form it takes.