Home
Backend from First Principles / Module 28 — Monolith vs Microservices

Monolith vs Microservices

When to split a system, the modular monolith middle ground, and the distributed monolith trap.


The Conventional Wisdom Has Shifted

For a decade, the default advice was: "if you're serious, you use microservices." Netflix and Amazon proved them at scale, conference talks evangelized them, every system design interview asked about them.

In 2026, the conversation has changed. Teams that rushed into microservices three years ago are quietly consolidating. Amazon Prime Video famously merged a microservices monitoring system back into a monolith and cut infrastructure costs by 90%. Segment, Istio, and others have published similar stories.

The new conventional wisdom: start with a well-organized monolith. Stay there as long as possible. Split only when you have specific evidence that the split will solve a real problem.

This isn't anti-microservices. It's anti-premature-distribution. The lesson is that microservices solve a specific class of problems — and create a different class of problems in return.


What a Monolith Actually Is

A monolith is a single deployable unit. One codebase, one process, one build, one deploy. All your features — auth, billing, notifications, search — live in the same application.

What people get wrong: "monolith" doesn't mean "tangled mess." A well-structured monolith has clear internal modules, just like a microservices system has services — except function calls between them are local instead of network calls.

Monolith strengths:
• Function calls are reliable and fast (microseconds, not milliseconds)
• One database, one transaction — atomic operations are easy
• Refactoring is safe — your IDE can rename across the whole codebase
• Debugging is one stack trace, not five log dashboards
• Local development is one process, not Docker Compose with 12 containers
• Deployments are atomic — there's no "what version is service B on?"

Monolith weaknesses (when scale becomes real):
• One slow endpoint can starve the others (shared resources)
• You can't scale individual hot paths separately
• Different teams stepping on each other's code
• Deploys take longer as the codebase grows
• Technology choice is uniform — can't adopt Rust for one piece


What Microservices Actually Are

Microservices split your system into many small services, each independently deployable. Auth is one service. Billing is another. Search is another. They communicate over the network — usually HTTP/REST or gRPC, sometimes async via a message broker.

Microservice strengths:
• Each service can scale independently (give billing 20 replicas, search 3)
• Each team owns its services end-to-end
• Failures can be contained — a billing outage doesn't kill auth
• Different services can use different languages or databases
• Deploys are independent — billing can ship without coordinating with everyone

Microservice costs (often underestimated):
• Every function call is now a network call — orders of magnitude slower and can fail
• You need distributed tracing, structured logs with correlation IDs, service mesh
• Local development requires running 10+ services
• Cross-service transactions are nearly impossible — you need sagas, eventual consistency
• "Where does this data live?" becomes a research question
• Operational complexity multiplies: 10 services means 10 deploys, 10 databases, 10 dashboards

Microservices are not a code-organization technique. They're an *operational model*. If your team can't operate distributed systems well, microservices will hurt more than help.


The Modular Monolith — The 2026 Default

The modular monolith is a single deployable application internally divided into strict modules. Like a microservices system in design, but a monolith in deployment.

Text
   ┌──────────────────── ONE DEPLOYED APP ────────────────────┐
   │                                                          │
   │   ┌──────────┐  ┌──────────┐  ┌──────────┐  ┌──────────┐ │
   │   │  Auth    │  │ Billing  │  │  Search  │  │Notify    │ │
   │   │  module  │  │  module  │  │  module  │  │ module   │ │
   │   └────┬─────┘  └────┬─────┘  └────┬─────┘  └────┬─────┘ │
   │        │             │             │             │       │
   │        └─────────────┴──────┬──────┴─────────────┘       │
   │                             │                            │
   │                    Module boundaries enforced            │
   │                    by code review and lint rules         │
   │                                                          │
   │                    Module B can only call Module A's     │
   │                    public interface, never its internals │
   │                                                          │
   └──────────────────────────────────────────────────────────┘

Rules for a real modular monolith:

  1. Each module has a clear public interface. Code outside that module imports only from that interface.
  2. Modules don't share database tables. Each module owns its data; others access it via the module's API.
  3. Lint rules or architecture tests enforce the boundaries — accidentally importing internals fails CI.
  4. Modules are organized around business domains (Auth, Billing, Search), not technical layers (Controllers, Services, Repositories).

The payoff: you get most of the microservices clarity with none of the network costs. And when one module genuinely needs to be extracted (because of independent scaling needs or team boundaries), the work is straightforward — the boundary is already drawn.

This is the architecture most teams should start with. Most teams should also stay there forever.


When to Actually Split

Microservices are the right answer when specific signals are real. Some signals to look for:

Signal 1: Independent scaling needs. Your image-processing pipeline genuinely needs 100x the compute of your API gateway. Splitting them lets you scale separately and saves real money.

Signal 2: Team coordination is the bottleneck. You have 8+ teams who all need to deploy the same monolith and they're stepping on each other. Conway's Law says: ship the org chart.

Signal 3: Fault isolation matters. A bug in your reporting feature should not be allowed to take down checkout. The blast radius of a bad deploy needs to be limited to one service.

Signal 4: Different technology genuinely needed. Your ML inference service really wants Python; your real-time pipeline really wants Rust. Forcing one stack on both costs more than splitting.

Signals that are NOT good reasons:
• "Netflix does it"
• "It feels more modern"
• "We might need to scale someday"
• "It'll be cleaner"

When you do split, follow the strangler fig pattern: extract one well-defined module to a service, prove the operating model works (deployment, observability, debugging), then extract the next. Never rewrite the whole thing at once.


The Distributed Monolith — The Worst of Both Worlds

The most common microservices failure mode isn't "we have too many services." It's "we have many services that act like one."

A distributed monolith looks like microservices on the org chart and acts like a monolith in production:

You've achieved maximum complexity with minimum benefit. According to industry research, ~90% of microservices teams batch-deploy their services together — meaning they paid the distribution cost without getting independent deployability.

The cure is hard: redraw service boundaries, give each service its own database, replace synchronous chains with async events, accept that some operations now span multiple deploys. Most teams either fix it over a year or two, or merge services back together until they have something simple again.

The takeaway: don't measure your architecture by the number of services. Measure it by how independently they can fail, deploy, and evolve. If they can't, you have one service in a trench coat.


⁂ Back to all modules