Skip to main content
SEMastery
Architecturebeginner

What Is a Modular Monolith? A Beginner-Friendly Guide for .NET

Understand the modular monolith in simple words: one app, strong internal walls. Learn how it compares to monoliths and microservices, why it is the 2026 default for most .NET teams, and how to build one.

12 min readUpdated December 3, 2025

A house with rooms, not one big hall

Imagine two houses.

The first house is a single huge hall with no inner walls. The kitchen, bathroom, bedroom, and living room all share one open space. At first this feels simple and cheap to build. But over time it becomes a nightmare: cooking smells reach your bed, guests in the living room walk through your bathroom, and changing one corner disturbs everything else. This is the classic big-ball-of-mud monolith — one app where all the code is tangled together.

The second option is to build many separate houses, one for each activity — a cooking house, a sleeping house, a bathing house — each on its own plot, connected by roads. Now things are nicely separated, but you have to walk outside and travel between houses for everything, maintain many roofs, and pay for many plots. This is microservices — powerful, but a lot of extra work.

A modular monolith is the smart middle choice: one house, but with proper rooms. You live in a single building (one app you deploy together), yet each room has clear walls and a door. The kitchen stays a kitchen; you do not walk through a wall to reach the bedroom. You get the tidiness of separate spaces without the cost of separate buildings.

That is the whole idea. Let us unpack it properly.

The three options side by side

To really understand a modular monolith, it helps to see all three architectures together.

Three Ways to Organize an Application

Traditional Monolith
Modular Monolith
Microservices

Steps

1

Traditional Monolith

One app, no clear inner boundaries — code gets tangled

2

Modular Monolith

One app, strong module walls — tidy and simple to run

3

Microservices

Many apps over the network — flexible but complex

A traditional monolith has no inner walls. Microservices are fully separate. A modular monolith is one deployable with strong inner walls.

The key insight: boundaries and deployment are two different things.

  • A traditional monolith has weak boundaries and one deployment.
  • Microservices have strong boundaries and many deployments.
  • A modular monolith has strong boundaries and one deployment.

People often think you must accept tangled code to get easy deployment, or accept complex deployment to get clean boundaries. The modular monolith proves you can have clean boundaries and easy deployment at the same time.

What makes a monolith "modular"

A modular monolith is not just a normal app with some folders. The "modular" part means real, enforced walls. Three rules make it work:

1. Each module owns its area. A module is a full vertical slice of one business area — for example, Orders, Billing, or Shipping. It contains its own logic, its own data, and its own rules. The Orders module is the single place that understands orders.

2. Modules talk only through public contracts. One module must never reach into another module's internal classes or database tables directly. If Shipping needs something from Orders, it asks through a clear public interface or a message — like knocking on a door instead of breaking through the wall.

3. The walls are enforced, not just hoped for. In .NET, you make each module a separate class library project and keep its internal types internal. Then other modules literally cannot see those internals — the compiler stops them. A small composition root in the main host project wires everything together at startup.

Figure 1: Modules communicate only through public contracts. Internal details stay hidden behind each wall.
💡

A simple test for a good module: could you delete every other module and still understand this one on its own? If yes, your boundaries are strong. If you cannot even open the file without dragging in half the codebase, the walls are too weak.

Why modular monoliths are the 2026 default

For years, microservices were treated as the "grown-up" choice, and teams jumped to them too early. In 2026 the mood has clearly shifted. The quiet, sensible trend is modular monoliths: one deployable, strict modules, fewer moving parts, faster debugging, and simpler data consistency.

Here is why so many .NET teams now start here:

  • Most teams do not need microservices. Roughly 70% of organizations do better with a well-designed modular monolith. They get clean architecture without distributed-systems pain.
  • It is far cheaper to run. Running, say, 15 microservices can cost 5–10× more than running one modular monolith at the same traffic, because each service needs its own hosting, network, and monitoring.
  • Debugging is easy. When everything runs in one process, you can follow a request from start to finish with a normal debugger. No hunting across ten services and network logs.
  • Strong consistency is simple. Because all modules share one database transaction when needed, you avoid the hardest part of microservices: keeping data consistent across the network.
  • It is the perfect launchpad. If you do grow into microservices later, clean module boundaries map directly onto service boundaries. You extract a module instead of untangling a mess.

The honest trade-offs

A modular monolith is not magic, and good engineering means knowing the limits:

  • One deployment for everyone. Every change ships together. You cannot deploy just the Billing module on its own — the whole app redeploys. For most teams this is fine; for very large organizations with many teams, it can become a bottleneck.
  • Scaling is all-or-nothing. You scale the whole app, not one hot module. If only Search is under heavy load, you still run more copies of everything. Microservices let you scale just the busy part.
  • Discipline is required. The walls only stay strong if the team keeps them strong. Without enforcement (separate projects, internal types, architecture tests), a modular monolith can slowly rot back into a tangled monolith.
⚠️

The biggest risk is silent erosion. One "quick" shortcut where Shipping reads an Orders table directly, repeated over a year, destroys your boundaries. Protect them with architecture tests that fail the build if a module reaches past its wall.

When to choose each architecture

Use this simple guide:

You should choose…When…
Traditional monolithA tiny app or prototype where structure is not worth the effort yet.
Modular monolithMost real business apps — you want clean boundaries, easy debugging, and low cost. Start here.
MicroservicesYou have hit real scaling walls, or many independent teams need to deploy and scale separately.

The wise default for a new .NET system in 2026 is the modular monolith. Start simple, keep strong walls, and only pay the microservices tax when your growth actually demands it.

The journey from monolith to microservices

A modular monolith is best understood as a stage in a system's life. Many teams travel this path:

The architecture journey

Big-ball-of-mud
Modular Monolith
Microservices

Steps

1

Start messy

An early app with tangled code

2

Add walls

Refactor into clear modules — one deployable

3

Split if needed

Extract modules into services under real load

Most systems are happiest in the middle stage. Move right only when real pressure demands it.

The beautiful part is that clean module walls make that last step gentle — each module lifts out almost as-is:

Figure 2: Because modules already talk through public contracts, extracting one into its own service barely changes the calling code.

Here is how the three stages compare on the things teams care about:

Big-ball-of-mudModular MonolithMicroservices
Code tidinessPoorGoodGood
Running costLowLowHigh
DebuggingHardEasyHard
Independent scalingNoNoYes

And the cost difference is stark when you picture the moving parts:

Figure 3: One modular monolith is a single thing to run; microservices multiply the hosting, network, and monitoring you must manage.

A quick look at building one in .NET

You do not need fancy tools. The pattern in .NET is:

MyApp.sln
├── MyApp.Host            // the single app you run — the composition root
├── Modules/
│   ├── Orders/
│   │   ├── Orders.Public    // contracts other modules may use
│   │   └── Orders.Core      // internal logic, hidden from others
│   ├── Billing/
│   │   ├── Billing.Public
│   │   └── Billing.Core
│   └── Shipping/
│       ├── Shipping.Public
│       └── Shipping.Core

Each module is its own set of projects. Other modules may reference only the *.Public project, never the *.Core one. The Host project references everything and wires it together at startup. With this layout, the compiler itself guards your walls — exactly what makes the monolith truly modular.

How modules actually talk to each other

If modules cannot reach into each other's internals, how do they cooperate? There are two clean ways, and both keep the walls strong.

1. Public contracts (synchronous). When Shipping needs an order's address right now, it calls a small public interface that Orders exposes:

// In Orders.Public — the only thing other modules may see
public interface IOrdersApi
{
    Task<AddressDto?> GetShippingAddress(Guid orderId);
}

Shipping depends on IOrdersApi, not on the real Order class or its table. The actual implementation lives hidden inside Orders.Core. This is like knocking on the kitchen door and asking for a glass of water, instead of climbing through the kitchen window.

2. In-process messages (asynchronous). Sometimes a module just wants to announce that something happened, without caring who listens. Orders can publish an OrderPlaced event, and Billing and Shipping react in their own time:

// Orders raises an event — it does not know or care who handles it
await _publisher.Publish(new OrderPlaced(order.Id));

Because everything runs in one process, this in-process messaging is fast and needs no network or broker. And here is the bonus: this is the same shape as messaging between microservices. So if you later split a module out into its own service, you swap the in-process publisher for a real broker — the calling code barely changes.

One database or many?

A question that comes up early: should each module have its own database? You have a flexible middle path that most teams use — one physical database, but separate schemas per module.

Each module gets its own schema (for example, orders.Orders, billing.Invoices), and a module only touches its own schema. This keeps tables clearly owned and prevents accidental cross-module reads, while still letting you use a single database transaction when two modules genuinely must change together. You get clean ownership without the headache of coordinating many separate databases. Later, if a module becomes a microservice, its schema lifts out into its own database cleanly — because nothing else was ever allowed to touch it.

Common mistakes that quietly rot a modular monolith

Most modular monoliths do not fail on day one. They rot slowly, one shortcut at a time. Watch for these:

  • The shared database free-for-all. If Shipping runs a SQL query straight against the Orders tables "just this once," the wall is gone. Each module should own its own tables, and others should never read them directly.
  • A giant "Shared" or "Common" project. It starts innocent, then becomes a dumping ground that every module depends on, secretly gluing everything back together. Keep shared code tiny and truly generic.
  • Public-by-default classes. If your types are public everywhere, nothing stops cross-module reach-ins. Make types internal by default and expose only deliberate contracts.
  • No enforcement. Hope is not a strategy. Add architecture tests (with a tool like NetArchTest) that fail the build if one module references another's internals. Now the rules guard themselves.
🚨

The day someone says "it is just a small shortcut across modules, we will clean it up later" is the day your modular monolith starts becoming a big ball of mud again. Treat boundary violations as build-breaking, not as code-review suggestions.

Quick recap

  • A modular monolith is one app you deploy together, split inside into modules with strong walls.
  • It separates two ideas people usually confuse: boundaries (how tidy your code is) and deployment (how many things you ship).
  • Modules own their area and talk only through public contracts, with walls enforced by separate projects and internal types.
  • It is the 2026 default for most .NET teams: clean, cheap, easy to debug, and a smooth launchpad to microservices later.
  • The trade-offs are one shared deployment, all-or-nothing scaling, and the need for discipline to keep the walls strong.

Like a well-planned house with proper rooms, a modular monolith gives you order and comfort without forcing you to build a whole neighbourhood. Start here, keep your walls strong, and you will have a system that is pleasant to work in today and easy to grow tomorrow.

References and further reading

Related Posts