Monolith, modular monolith, or microservices?
How to pick an architecture for your next project, and why the honest answer usually starts smaller than you expect.
Every project starts with the same question, and it is almost always asked too early: how should we split this up?
The industry answer for the last decade has been "microservices". Having built and run a .NET microservices system end to end — a dozen services, a gateway, a message bus, live deployments — my answer is more boring. Start with the smallest thing that can work, and let real pressure, not anticipated pressure, decide when to split.
The three options, honestly
Monolith
One deployable. One database. One build.
A monolith is not a mistake, and it is not "legacy". It is the architecture with the
fewest moving parts, and moving parts are what you pay for every day afterwards. A
function call cannot time out, arrive twice, or arrive out of order. A transaction across
two tables is one SaveChanges, not a saga.
The real failure mode is not size, it is entanglement: when the orders code reaches into the products tables directly, and three years later nothing can be changed in isolation.
Modular monolith
One deployable, with enforced internal boundaries.
This is the option most teams should reach for and most teams skip. You keep the single-process simplicity, but modules talk through defined interfaces rather than each other's internals. Each module owns its own tables and nothing else touches them.
// The boundary is the point. Orders asks Products a question;
// it does not reach into the products schema to answer it itself.
public interface IProductCatalog
{
Task<ProductDto?> GetAsync(int productId, CancellationToken cancellationToken);
}
If you enforce that — with separate projects, internal types, and an architecture test that fails the build on a forbidden reference — you get most of the benefit of services and almost none of the cost. And when you do split one out later, the seam already exists.
Microservices
Many deployables, each owning its own data, communicating over the network.
Microservices solve organisational and operational problems, not code-quality problems. They are worth it when teams need to deploy independently, when one component's scaling profile is genuinely different, or when isolating failure matters more than simplicity.
They are not worth it because the code feels messy. A distributed system built out of messy code is messy code you now have to debug across a network.
What you actually pay for
The cost of splitting is not writing the services. It is everything around them.
| Concern | Monolith | Microservices |
|---|---|---|
| Calling another module | Method call | Network call that can fail, retry, duplicate |
| Data consistency | One transaction | Sagas, outbox, eventual consistency |
| Local development | Press F5 | Run several services, or fake them |
| A bad deploy | One rollback | Which of nine services? |
| Debugging a request | One stack trace | Correlation ids across services |
| Schema change | One migration | Versioned contracts, two deploys |
None of these is a reason not to use microservices. They are the invoice, and it is worth reading the invoice before signing.
The questions worth asking
Not "is this modern?" but:
- Do separate teams need to deploy without coordinating? This is the strongest genuine reason to split. One team, one deployable.
- Does one part have a genuinely different scaling profile? An ML inference endpoint that needs different hardware to a CRUD API is a real boundary.
- Must one part fail without taking the rest down? If checkout has to survive the recommendation engine falling over, that is a real isolation requirement.
- Can you afford the operational floor? Centralised logs, tracing, CI/CD per service, health checks, a story for configuration and secrets. That floor is roughly the same whether you have three services or thirty.
If the honest answer to 1 through 3 is no, a modular monolith will serve you better, and you can still split later. The reverse — recombining services after discovering the split was wrong — is much harder.
Where the seams belong
When you do split, split along business capabilities, not technical layers.
A service called Orders that owns ordering end to end is a boundary. A service called
DataAccessService that every other service calls is a distributed layer, which gives you
all the network cost with none of the independence — every change still ripples through
everything.
The test is simple: can this service be deployed without deploying anything else? If changing a field means coordinating three deploys, the boundary is in the wrong place.
What I would do on a new project
- Small team, unproven product — modular monolith, one database, strict module boundaries. Ship, learn, and keep the seams clean.
- Known domain, several teams, different scaling needs — microservices along business capabilities, but only for the parts that need it. A hybrid is fine and common.
- Anything with an "and it must scale to millions" in the first sprint and no users yet — build the modular monolith. You do not yet know which part needs to scale, and guessing wrong is the expensive outcome.
The part nobody mentions
Microservices make your architecture diagram look impressive and your Tuesday afternoon harder. That trade is worth making when the organisation genuinely needs it. It is a bad trade when it is made to look current.
Pick the boring option. Keep the boundaries clean. Split when something actually hurts — and when it does, you will know exactly where the seam goes, because you will have been living with it.