System Design
Monolith vs Microservices vs Modular Monolith (and Why Not to Start With Microservices)
Microservices are the answer to a question most teams haven't earned yet. Here's how to tell which architecture you actually need - and why the trendy one is usually wrong at the start.
A candidate once told me, with total confidence, "you should always use microservices." I asked why. He said "because they scale." I asked what he'd be giving up to get that scaling, and the room went quiet - because in his head, microservices weren't a trade-off, they were simply the better option. An upgrade. The grown-up choice.
That belief is everywhere, and it's expensive. I've spent sixteen years on both sides of this. Early in my career I worked on enormous enterprise monoliths at IBM. Later, at Stripe and Amazon, I worked inside service-oriented architectures running at genuinely massive scale. I've watched startups tear a perfectly good monolith into thirty services they couldn't operate, and I've watched large organizations cling to a monolith long past the point where it was strangling them. Both directions hurt.
So let me give you the version of this debate that survives contact with reality - what each architecture actually is, what it costs, the underrated middle option most people forget exists, and why "start with microservices" is, nine times out of ten, the wrong instinct.
What is a monolith?
A monolithic application is built and deployed as a single unit. All the code - user accounts, payments, notifications, search, everything - lives in one codebase, compiles into one artifact, and runs as one process (or many identical copies of that one process behind a load balancer). When you deploy, you deploy the whole thing.
This is how almost every application starts, and for good reason. It's the simplest thing that works.
Inside a monolith, your "services" are just function calls. The payments code calls the notifications code directly, in memory, in the same process. There's no network in between, no serialization, no "what if the notification service is down." It either works or it throws an exception you can catch, with a stack trace that tells you exactly where the problem is.
Pros:
- Simple to build, test, and reason about - it's one program
- Fast in-process calls; no network latency between components
- Easy debugging - one stack trace spans the whole request
- Straightforward deployment - ship one artifact
- Transactions across the whole app are trivial (one database)
Cons:
- The whole app must be deployed together - one small change ships everything
- A bug in one area can crash the entire process
- Scaling is all-or-nothing - you scale the whole app even if only search is hot
- Over time, a large team stepping on one codebase slows everyone down
- The codebase can rot into a "big ball of mud" without discipline
The classic knock on monoliths is that they "don't scale." That's misleading. A monolith scales technically very well - you run more copies behind a load balancer, and modern monoliths happily serve enormous traffic. What a monolith scales poorly is teams. When fifty engineers are all committing to the same codebase and deploying through the same pipeline, they start tripping over each other. That organizational friction, not raw request volume, is the real ceiling.
What are microservices?
A microservices architecture breaks the application into many small, independent services, each owning one capability and each deployed separately. Payments is its own service with its own database and its own deploy pipeline. Notifications is another. Search is another. They communicate over the network - typically HTTP/REST, gRPC, or asynchronous messages on a queue.
The defining property isn't smallness; it's independence. Each service can be built by a different team, written in a different language, scaled to a different size, and deployed on its own schedule without coordinating with anyone else.
graph TB
subgraph MONO[Monolith - one deployable]
direction TB
M[Users · Payments · Search · Notifications<br/>all in one process]
M --> MDB[(Single Database)]
end
subgraph MICRO[Microservices - independent deployables]
direction TB
G[API Gateway] --> S1[Users svc] & S2[Payments svc] & S3[Search svc]
S1 --> D1[(Users DB)]
S2 --> D2[(Payments DB)]
S3 --> D3[(Search DB)]
end
This diagram contrasts two architectures. On the left, a monolith runs users, payments, search, and notifications all in one process, backed by a single shared database. On the right, a microservices architecture has an API gateway routing to independent services - users, payments, and search - each owning its own separate database and deployable independently.
That independence is genuinely powerful at the right scale. But look closely at the right side of that diagram and you'll see what you've signed up for: the simple in-memory function call from the monolith is now a network request that can be slow, can fail, can time out, and crosses a database boundary. You haven't removed complexity - you've moved it from inside your code to between your services, where it's much harder to see and debug.
Pros:
- Independent deployment - teams ship on their own cadence
- Independent scaling - scale only the services under load
- Fault isolation - one service failing needn't take down the others
- Technology freedom - each service picks its own stack
- Team autonomy - clear ownership boundaries at scale
Cons:
- Distributed-systems complexity: network calls fail, time out, and lag
- Data consistency across services is genuinely hard (no easy transactions)
- Operational overhead: many deployments, dashboards, logs to correlate
- Debugging spans services - one request touches five logs in five places
- Network latency and serialization add real cost to every cross-service call
The third option everyone forgets: the modular monolith
Here's the part of this debate that's chronically missing. People frame it as a binary - messy monolith versus clean microservices - when the most pragmatic architecture for most teams sits right in the middle.
A modular monolith is a single deployable, like a monolith, but with strong internal boundaries between modules, like microservices. The payments module and the notifications module live in the same process and ship together, but they're separated by clear, enforced interfaces - they only talk to each other through well-defined contracts, not by reaching into each other's internals.
You get most of the discipline of microservices - clean boundaries, clear ownership, modules that could become services later - without paying the distributed-systems tax. Calls between modules are still fast in-process calls. You still have one database and easy transactions. You still deploy one artifact and get one stack trace. But the codebase doesn't rot into a big ball of mud, because the boundaries are real.
Tip: The modular monolith is the architecture I wish more teams reached for. It keeps your options open. If a module later genuinely needs to be its own service - its own scaling profile, its own team, its own deploy cadence - you extract it, and because the boundary was already clean, the extraction is straightforward. You've earned the right to a microservice by proving the boundary first.
Why NOT to start with microservices
Now the heart of it. The instinct to begin a new system with microservices is one of the most common and costly mistakes I see - in interviews and in real engineering organizations. Here's why it backfires.
1. You're taking on distributed-systems problems you don't need yet. The moment you split into services communicating over a network, you inherit every hard problem of distributed systems at once: network calls that fail and time out, retries and idempotency, data consistency across service boundaries, distributed tracing to follow a request, service discovery, and more. A monolith makes all of these disappear into a single function call. Starting with microservices means solving the hardest problems in software engineering before you've even validated that your product works.
2. You don't know your boundaries yet. Good service boundaries follow the natural seams of your domain - but early on, you don't know where those seams are. The product is still changing. If you draw service boundaries too early and draw them wrong, you end up with services that are tightly coupled and constantly need to change together. Which leads directly to the worst outcome of all...
3. The distributed monolith - worst of both worlds. This is what you get when you split into microservices but the services are so interdependent that you can't deploy one without deploying the others. Now you have all the operational pain of microservices - network calls, many deployments, distributed debugging - with none of the independence that was supposed to justify it. You've taken the simplicity of the monolith and the complexity of microservices and kept the wrong half of each. I've seen teams spend a year building one of these and not realize what they'd done until it was deeply entrenched.
4. Operational cost is real and immediate. Microservices need infrastructure: container orchestration, service meshes, centralized logging, distributed tracing, per-service CI/CD, monitoring for dozens of services instead of one. A three-person startup running thirty microservices is spending most of its energy operating infrastructure instead of building product.
Warning: "We'll go microservices from day one so we don't have to migrate later" is a trap. You will almost certainly draw the boundaries wrong before the product stabilizes, and re-drawing service boundaries is far more painful than extracting a service from a well-organized monolith. Premature decomposition costs more than it saves.
When microservices DO make sense
None of this means microservices are bad. They're the right answer to specific problems - problems you should actually have before adopting them:
- Team scale. This is the real driver. When you have many teams who need to deploy independently without coordinating, microservices give each team autonomy. This connects to Conway's Law - that systems tend to mirror the communication structure of the organization that builds them. Microservices let you align service boundaries with team boundaries on purpose.
- Independent scaling needs. When one part of your system has a wildly different load profile than the rest - say, a search service that needs ten times the capacity of everything else - splitting it out lets you scale it independently instead of scaling the whole monolith.
- Independent deploy cadence. When different parts of the system change at very different rates, and you don't want a tiny change in one area to require redeploying everything.
- Fault isolation at scale. When it's worth real complexity to ensure one component's failure can't take down the others.
Notice that every one of these is a problem of scale - of team size, of load, of organizational complexity. A small team building a new product has none of them yet.
The path I'd actually recommend
The sensible evolution, the one that's served the strongest teams I've worked with:
- Start with a monolith. Ship the product. Learn the domain. Move fast while everything is simple.
- Keep it modular as it grows. Enforce clean module boundaries inside the monolith - make it a modular monolith - so the codebase stays organized and the natural seams of your domain start to reveal themselves.
- Extract services when the pain is real and specific. When a particular module genuinely needs independent scaling, an independent deploy cadence, or a dedicated team, extract that module into a service. Because you kept the boundary clean, the extraction is clean. You let evidence, not fashion, drive the decomposition.
This is sometimes called "monolith first," and it's not a compromise - it's the disciplined path. Even Amazon and Netflix, the companies whose names get invoked to justify microservices, largely evolved into them as they hit genuine scale problems. They didn't start thirty-person startups with two hundred services.
Interview: When a system design interview touches architecture, the answer that earns a strong signal is almost never "microservices, because scale." It's: "I'd start with a modular monolith - it's simpler, faster to build, and I don't yet know the right service boundaries. I'd extract services later, when a specific module needs independent scaling or a dedicated team owns it." That answer shows you understand microservices are a trade-off with a real cost, not a free upgrade - which is exactly what separates an engineer who's operated distributed systems from one who's only read about them.
How to know when it's time to extract a service
The hardest part of the "monolith first" path isn't starting with a monolith - it's recognizing the moment a piece of it has genuinely earned promotion to a standalone service. Extract too early and you pay the distributed-systems tax for nothing; extract too late and the monolith becomes the bottleneck everyone complains about. Watch for these concrete signals:
- A module has a wildly different scaling profile. If your search functionality needs ten times the compute of everything else, and you're forced to scale the entire monolith just to give search more headroom, search has earned its own service. You'll save money and gain control by scaling it independently.
- A module changes on a completely different cadence. If one part of the system ships ten times a day while the rest ships weekly, bundling them means the fast-moving part is held hostage by the slow-moving one's release schedule. Separation lets each move at its natural speed.
- A clear team boundary has formed. When a distinct team has taken ownership of a capability and wants to deploy without coordinating with everyone else, a service boundary aligned to that team boundary removes friction - this is Conway's Law working in your favor rather than against you.
- A module needs different technology. If one capability would be dramatically better in a different language or runtime - say, a compute-heavy component that belongs in a systems language - isolating it as a service lets it use the right tool without rewriting everything around it.
- A module's failure must be isolated. If one feature failing absolutely cannot be allowed to take down the core product, pulling it into a separate service with its own failure domain provides that isolation.
Notice what's not on this list: "because microservices are modern," "because a big company uses them," or "because we might need to scale someday." Those are fashion, not signals. The discipline is to wait for real, specific, present-tense pain - and then extract the one module causing it, not blow up the whole architecture at once.
Caution: Extracting a service is itself work with risk. You're introducing a network boundary, splitting data, and adding operational overhead. Do it deliberately, one service at a time, with the clean boundary you maintained in your modular monolith making each extraction as painless as possible. A series of careful, evidence-driven extractions beats one big-bang rewrite every time.
The candidate who told me "always microservices" wasn't wrong about what microservices do. He was wrong about cost - he saw only the benefit and none of the tax. That's the whole lesson of architecture: there is no best, only fit. The right call depends on your team size, your scale, and how well you actually understand your domain yet. We work through exactly these decisions - with the trade-offs spelled out, on realistic prompts - across System Design Fundamentals.
Go deeper
- Microservices - Martin Fowler - the canonical, balanced description of the style and its costs.
- Monolith First - Martin Fowler - the argument for starting monolithic and extracting later.
- Microservices (overview) - the broader landscape, patterns, and anti-patterns.
- Conway's Law - why your architecture ends up mirroring your org chart.