Stop Building Monoliths! Build Modular Monoliths Instead
Intro to Modular Monolith Architecture
Today we’ll talk about Modular Monolith architecture — what it is, the problems it solves, and why I believe it’s the right starting point for many modern applications. We’ll also walk through a few diagrams and a real demo app I’ve built to see how this looks in an actual codebase.
What is a Modular Monolith?
A Modular Monolith is a software architecture that runs as a single process, just like a traditional monolith. The difference is that instead of one large, tangled codebase, the application is explicitly split into well-defined modules.
Each module represents a separate domain and owns its own logic. The boundaries are clear, enforced in code, and intentional — not just folders pretending to be architecture, but separate class libraries.
What is a Module?
A module is a self-contained logical unit that represents a specific business capability (for example, Payments or Reviews). Its sole purpose is to model that specific business domain — exactly how you would design a microservice.
Each module behaves like a separate application focused on a single business use case. It has its own API layer and its own data store (a separate schema or even a separate database). In other words, it’s an independent application that happens to live inside the monolith, and it communicates with other modules only through its public API.
And this is the key point: even though all modules run inside a single process, they are designed so that, if needed, they can later be extracted into independent microservices with minimal friction.
Here’s how a typical module looks like.
Each module exposes its own public API, and that is the only way other modules are allowed to access its logic. This is extremely important because it enforces loose coupling, making modules easier to extract later — which is at the core of the Modular Monolith’s benefits.
In addition, every module owns its own data store — either a separate schema within a shared database or a completely separate database. Data ownership is strict and enforced at the module boundary.
In short, think of each module as an independent API that just happens to live inside the same application.
Why use Modular Monolith?
A Modular Monolith is powerful because it lets you start simple, while still enforcing clear boundaries that allow the system to scale later if — and only if — it actually becomes necessary.
You get the simplicity of a monolith today, but by designing proper modules, you’re deliberately preparing for a future where parts of the system may need to be distributed into microservices. If that time comes, the transition is far less painful than trying to break apart a traditional, tightly coupled monolith.
Yes, you can start directly with a microservices architecture. But in most cases, that’s a bad idea, even when you’re convinced you’ll eventually need it (this is exactly what Monolith First by Martin Fowler argues).
By building a Modular Monolith, you get several concrete benefits that help you ship faster and at lower cost — which, in today’s enterprise world, is non-negotiable (everyone wants it yesterday):
Easy to start with — it’s still a monolith
Simple CI/CD — a single deployable unit
Clear boundaries — making future migration to microservices significantly easier
Easier to scale when needed — both technically and organizationally
Better maintainability and team ownership — teams work on modules without stepping on each other
Real Estate Platform
Let’s take a fictional real estate platform as an example — a place where people list their homes for others to buy. In this scenario, a Modular Monolith solution could look like this.
At this point, it’s important to understand what this diagram does not represent.
This is not a microservices architecture. All modules run inside a single process, are deployed together, and share the same runtime. There is no network communication between them, no service discovery, no distributed tracing, and no operational overhead.
Yet, from a design perspective, each module behaves as if it were a microservice.
Each module:
Owns its business logic
Exposes a public API
Owns its data
Has no direct access to other modules’ internals or databases
This is what gives the Modular Monolith its power: strong boundaries without distribution.
Module-to-Module Communication
So how do modules talk to each other? Let’s take Listings and Reviews as an example.
Imagine we’re building a page that needs to display a specific listing (for example, a house on XYZ Street) along with all the reviews people have left for it.
The Listings module should not reach directly into the Reviews database, and the Reviews module should not query Listings data directly. Instead, communication happens strictly through each module’s public API.
The application first calls the Listings module to fetch the listing data by listingId. Then, using the same identifier, it calls the Reviews module through its public API to retrieve all reviews associated with that listing.
Both modules:
Access only their own database
Expose explicit read models through their APIs
Remain completely unaware of each other’s internal implementation
Even though these calls happen inside the same process, they are treated exactly like service-to-service calls. No shared repositories. No cross-module ORM access.
This discipline is what keeps the system modular.
If, at some point, the Reviews module needs to be extracted into a separate microservice, this flow remains largely unchanged — the in-process API call simply becomes a network call. The contract stays the same.
This is the core idea behind Modular Monolith communication: design for distribution, without paying the distribution cost upfront.
Modular Monolith Solution
To make this concrete, here’s how this looks in a real codebase.
The solution is structured around a Modules folders, where each module represents a distinct business capability — Accounts, Listings, Reviews, and so on.
Each module is fully self-contained and separate class library that follows the same internal structure built around Clean Architecture:
API — the module’s public surface, exposed to other modules
Application — use cases, orchestration, and business workflows
Domain — core business logic and domain models
Infrastructure — data access, external integrations, persistence
This is not accidental. Every module is treated as a mini application with clear ownership and explicit boundaries.
The Shared projects contain only truly cross-cutting concerns — things like the database abstraction, shared kernel primitives, or mediator implementation (a custom one I built since MediatR is now commercial). There is no shared business logic between modules.
At the top level, there’s a single API host that wires everything together and a single Web UI consuming the system — reinforcing the fact that this is still a single deployable unit, running in a single process.
This structure enforces discipline:
Modules don’t reach into each other’s internals
Data access stays inside module boundaries
Communication happens only through public APIs
And most importantly, if one of these modules ever needs to become a standalone microservice, most of the work is already done — the boundaries, contracts, and responsibilities are already in place.
That’s the real power of a Modular Monolith: microservice-level design, without microservice-level complexity.
Lastly, let’s take a deeper look at one of the modules — the Listings module.
At first glance, you’ll notice that the module follows the same internal structure as the others: API, Application, Domain, and Infrastructure. This is intentional and consistent across all modules, reinforcing clear boundaries and ownership.
API
The API project is the module’s public entry point. It exposes endpoints (for example, ListingsEndpoints.cs) and defines how the outside world — including other modules — can interact with Listings. Nothing outside the module talks directly to its internals.
Application
The Application layer contains the use cases and orchestration logic. This is where the module’s behavior lives — not in controllers, not in infrastructure.
The important part here is the Features folder.
Each feature (like CreateListing, GetListingById, or GetUserListings) is implemented as a vertical slice — meaning everything related to that use case lives together. This follows Jimmy Bogard’s Vertical Slice Architecture.
Instead of grouping code by technical concerns (controllers, services, handlers), we group it by business intent.
Each feature typically contains:
The request/command
The handler
Validation
Any feature-specific logic
This makes features easy to understand, easy to modify, and easy to delete.
Domain
The Domain project contains the core business logic:
Entities
Value Objects
Domain Events
This layer is pure business logic. It has no dependencies on infrastructure, frameworks, or external concerns. It represents the heart of the Listings domain.
Infrastructure
The Infrastructure project handles everything related to persistence and external systems — for example, database access and ORM configuration. This is the only place where the module touches technical details like EF Core.
Why this works so well
This combination — Modular Monolith + Clean boundaries + Vertical Slice Architecture — gives you:
Strong separation between business domains
Highly focused, readable features
Minimal coupling between modules
A structure that scales without becoming unmanageable
Each module is cohesive. Each feature is explicit. And the entire system remains a single, simple deployable unit.
Final Words
I hope this post helps you appreciate the Modular Monolith approach — and gives you a clear idea of how to build one.
I strongly believe you should always start small and avoid overengineering. Every large company started small, and they only scaled when it became necessary. Don’t start with expensive decisions. Build iteratively.
Don’t begin with the idea of building the next Netflix. Start with a basic streaming app, then add one feature, then another — but keep it simple. Once you see real demand, then you can justify adding complexity. Not before.
This is exactly why Modular Monolith fits my mindset so well. It keeps you on the right track: a single-process application, built with clear module boundaries around real business capabilities. And if your product succeeds and you truly need to scale out, you can evolve the right modules into microservices with far less pain.






