The first thing many developers notice about a modular monolith is the folder structure.
You see something like:
Modules/
Events/
Users/
Ticketing/
and it looks straightforward.
Create some folders. Put related features together. Call them modules.
But while revisiting modular monolith architecture, one thing became clearer to me:
A folder doesn’t create a module. Ownership does.
A meaningful module should own more than the location of its source files. It should establish boundaries around its behavior, composition, persistence, and data.
That distinction sounds small. Architecturally, it isn’t.
The application host shouldn’t know everything
Consider a host application with an Events module.
The application’s startup can remain remarkably small:
builder.Services.AddEventsModule(builder.Configuration);
WebApplication app = builder.Build();
EventsModule.MapEndpoints(app);The host knows that an Events module exists, but it doesn’t need to know every implementation detail inside that module.
The module itself can own its registration and endpoint mapping.
Instead of allowing Program.cs to gradually become the
place where every feature registers its database, services, endpoints,
handlers, and infrastructure, the module encapsulates those details.
The host becomes the composition root for modules, rather than the place where every implementation detail in the system is wired together.
That’s a subtle but useful distinction.
Data ownership is part of modularity
The more interesting part comes when persistence enters the picture.
A module can have its own DbContext and its own database
schema:
public sealed class EventsDbContext(
DbContextOptions<EventsDbContext> options)
: DbContext(options)
{
internal DbSet<Event> Events { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.HasDefaultSchema(Schemas.Events);
}
}Notice what is happening.
The module doesn’t merely own some C# classes. It owns a persistence boundary.
Its tables can live under an events database schema, and
its persistence configuration stays with the module.
Conceptually:
Application
│
├── Events Module
│ ├── Events behavior
│ ├── Events endpoints
│ ├── EventsDbContext
│ └── events schema
│
├── Users Module
│ └── ...
│
└── Ticketing Module
└── ...
The modules may still use the same physical PostgreSQL database. That’s one of the practical advantages of a modular monolith: we don’t immediately pay the operational cost of distributed services.
But logically, we’re beginning to establish ownership.
That leads to a question I find more useful than simply asking whether a class is in the correct folder:
Which module owns this data?
If the answer isn’t clear, the module boundary probably isn’t clear either.
Modular doesn’t automatically mean distributed
This is where modular monoliths become particularly interesting.
We can establish meaningful boundaries without immediately introducing separate services, message brokers, service discovery, network failure modes, distributed tracing, and deployment coordination.
Instead, we can retain a single deployable application while creating internal business boundaries.
Application
│
┌────────────┼────────────┐
│ │ │
Events Users Ticketing
│ │ │
└────────────┼────────────┘
│
Database
That’s why I increasingly think the interesting question isn’t simply:
Monolith or microservices?
It’s:
How well defined are the boundaries of the system?
A badly structured monolith can become painful. But distributing poorly defined boundaries across multiple services doesn’t magically fix them.
Sometimes it simply turns unclear boundaries into network calls.
Modules and vertical slices solve different problems
Another useful idea appears in how use cases can be organized inside a module.
Instead of organizing everything primarily by technical type:
Controllers/
Services/
Repositories/
DTOs/
we can organize around use cases:
Events/
CreateEvent.cs
GetEvent.cs
This starts to look like Vertical Slice Architecture.
And this is where two architectural ideas that are sometimes discussed separately fit together nicely.
A module defines a business boundary.
A vertical slice defines a use-case boundary inside that business capability.
Events Module
│
├── Create Event
├── Get Event
├── Publish Event
└── Cancel Event
The two concepts aren’t competing architectures. They operate at different levels.
The module answers:
Where does this business capability belong?
The vertical slice answers:
What does this particular use case need?
I find that mental model much easier to reason about than forcing every application into horizontal technical layers.
Reads don’t always need the domain model
A query can also project directly into the response it needs:
EventResponse? @event = await context.Events
.Where(e => e.Id == id)
.Select(e => new EventResponse(
e.Id,
e.Title,
e.Description,
e.Location,
e.StartsAtUtc,
e.EndsAtUtc))
.SingleOrDefaultAsync();That’s worth noticing.
For a read operation, the objective is often simply the data required by the caller. We don’t necessarily need to reconstruct a rich domain object merely to return a DTO.
This connects with something I wrote about previously when discussing the Repository Pattern with EF Core.
Reads and writes don’t necessarily have the same requirements.
A write operation may need:
Command
↓
Domain behavior
↓
Aggregate
↓
Persistence
while a read operation may be perfectly comfortable with:
Query
↓
EF Core
↓
Projection
↓
DTO
The important part is not forcing both paths through identical abstractions simply because symmetry looks architecturally pleasing.
You don’t need every abstraction on day one
There’s another lesson in what a first module doesn’t need.
It doesn’t need a generic repository around every entity. It doesn’t need an interface for every class. It doesn’t need an elaborate mediator pipeline or dozens of projects representing architectural layers.
And yet meaningful boundaries can already start forming.
That reinforces another lesson for me:
Good architecture doesn’t require implementing every pattern on day one.
Architecture can evolve as the system earns complexity.
Start by protecting the boundaries that already matter. Introduce additional abstractions when they solve an actual problem.
This is something experienced developers can occasionally forget because we already know many patterns.
Knowing a pattern makes it tempting to use it.
That doesn’t mean the application needs it yet.
The database can be shared without the data becoming shared
One of the most important distinctions in a modular monolith is between sharing infrastructure and sharing ownership.
Two modules may physically store their tables in the same PostgreSQL database. That does not necessarily mean every module should freely manipulate every table.
The physical deployment might be:
PostgreSQL
│
├── events schema
├── users schema
└── ticketing schema
But logically:
Events → owns Events data
Users → owns Users data
Ticketing → owns Ticketing data
That distinction matters.
Otherwise, the application may look modular in the solution explorer while remaining tightly coupled through the database.
That’s what I think of as a folder-based modular monolith.
It looks modular. The dependencies tell a different story.
The question I’m starting to ask
When reviewing an architecture, I’m finding this question increasingly useful:
What does this module actually own?
Not merely which folder contains a class, or which project should contain an interface, but what behavior and data the module owns, how it is composed, what other modules can access, and eventually how those modules should communicate.
Those questions tell us much more about modularity than the folder structure does.
And as the application grows, the answers become increasingly important.
What I took away from building the first module
The first module of a modular monolith doesn’t need to demonstrate every sophisticated architectural pattern.
It needs to establish the beginning of ownership.
The application host composes modules. The module owns its endpoints and persistence configuration. The module begins owning its data. Use cases can be organized vertically inside that boundary. Reads can project directly to the models they require. Additional abstractions can arrive when the system actually needs them.
The lesson I’m taking away is simple:
Modularity isn’t primarily about where code lives. It’s about who owns what.
Folders can help communicate that architecture. Projects can help enforce it. Database schemas can reinforce it.
But none of those things create the boundary by themselves.
The boundary becomes meaningful when the system has a clear answer to:
Who owns this?
And that’s a question worth asking long before deciding whether a system needs microservices.
Further Learning
This article was inspired by concepts I’m revisiting while studying Milan Jovanović’s Modular Monolith Architecture course and thinking about how those ideas apply to systems I build and maintain.
The course provides the learning context; the observations and conclusions here are my own interpretation of those concepts.
Explore Milan Jovanović’s Modular Monolith Architecture course →<
Disclosure: The course link above is an affiliate link. If you purchase through it, I may earn a commission at no additional cost to you. I’ve personally enrolled in Milan’s architecture courses and use them as part of my continuing software architecture learning.
Learning the Next Thing.
