Monolith vs Microservices
The decision
Choosing between a monolithic architecture and microservices is one of the most consequential early decisions in software system design. A monolith deploys a single executable unit where all application logic runs in the same process, shares the same database, and is deployed together. Microservices decompose that unit into independently deployable services, each owning its own data and communicating over a network.
This decision is rarely permanent, but reversing it is expensive. Teams that start with microservices prematurely pay a steep distributed systems tax—network failures, eventual consistency, distributed tracing, service discovery—before they have the domain knowledge to draw correct service boundaries. Teams that wait too long to decompose accumulate a tangled codebase where a single change requires touching dozens of files across the entire system.
A third option—the modular monolith—sits between the two extremes. It enforces strict module boundaries within a single deployable unit, giving you the simplicity of a monolith while building the domain knowledge needed to eventually extract services cleanly. Many successful architectures remain modular monoliths forever; microservices are not the default destination.
Why it matters
The wrong choice here doesn't just affect performance or scalability—it directly impacts team velocity, system reliability, and operational complexity. Premature microservices decomposition has caused numerous high-profile engineering failures, with teams spending months building infrastructure (service meshes, distributed tracing, Kubernetes clusters) instead of delivering product value. The overhead of running ten services is not ten times the overhead of one; distributed systems introduce entire new classes of failure that don't exist in a monolith.
Conversely, a monolith that has grown beyond its natural size becomes a deployment bottleneck. A bug in the billing module blocks a release of the recommendation engine. All teams must coordinate deployments. A single memory leak takes down every feature simultaneously. The monolith has transitioned from an asset into a liability.
The real cost is in organizational alignment. Conway's Law dictates that your system architecture mirrors your communication structure. If you have ten teams that can work fully independently, microservices may be appropriate. If you have a single team of six engineers, microservices multiply your coordination cost without delivering independence benefits.
Choose a Monolith when
- The team has fewer than 10-15 engineers with unified ownership of the codebase
- The domain is not yet well-understood and service boundaries are unclear or frequently changing
- The product is in early-stage development where speed of iteration matters more than scalability
- Operational maturity is low and the team lacks experience running distributed systems in production
- The application does not have components with radically different scaling requirements
- Strong consistency and ACID transactions across operations are required throughout the system
- Time-to-market is the primary concern and infrastructure overhead would delay release significantly
Choose Microservices when
- Multiple independent teams need to deploy features without coordinating with other teams
- Domain boundaries are well-understood and stable, allowing clean service decomposition
- Different components have genuinely different scaling, availability, or technology requirements
- The organization has Conway's Law working in its favor—team structure maps to service ownership
- Independent deployment is a hard requirement, such as regulated components needing separate audit trails
- The system has proven bottlenecks that cannot be addressed within a monolith's scaling model
- Fault isolation is critical—a failure in one service must not cascade to others
Comparison
MONOLITH MICROSERVICES
════════════════════════════ ════════════════════════════════════════
┌──────────────────────────┐ ┌──────────┐ ┌──────────┐ ┌────────┐
│ Application │ │ Orders │ │ Users │ │Catalog │
│ ┌──────┐ ┌──────────┐ │ │ Service │ │ Service │ │Service │
│ │Orders│ │ Users │ │ └────┬─────┘ └────┬─────┘ └───┬────┘
│ └──────┘ └──────────┘ │ │ │ │
│ ┌──────┐ ┌──────────┐ │ ─────┴──────────────┴─────────────┴───
│ │Notify│ │ Catalog │ │ Message Bus / HTTP
│ └──────┘ └──────────┘ │ ─────┬──────────────┬─────────────┬───
└────────────┬─────────────┘ │ │ │
│ ┌────┴────┐ ┌─────┴───┐ ┌─────┴───┐
┌────────┴────────┐ │Orders DB│ │Users DB │ │Catalog │
│ Shared DB │ └─────────┘ └─────────┘ │ DB │
└─────────────────┘ └─────────┘
Deployment: single artifact Deployment: independent per service
Scaling: scale everything together Scaling: scale each service independently
Failures: one failure affects all Failures: isolated per service boundary
Transactions: ACID across all ops Transactions: saga / eventual consistency
Network calls: in-process (fast) Network calls: over-the-wire (latency)
Debugging: single process trace Debugging: distributed tracing required
CI/CD: one pipeline CI/CD: many pipelines (DevOps overhead)
MODULAR MONOLITH (middle ground)
═════════════════════════════════
┌────────────────────────────────┐
│ ┌──────────┐ ┌─────────────┐ │ • One deployable unit
│ │ Orders │ │ Users │ │ • Strict interface boundaries enforced
│ │ Module │ │ Module │ │ • No direct DB table cross-access
│ └─────┬────┘ └──────┬──────┘ │ • Easy to refactor into services later
│ │ Public APIs │ │ • Recommended for most teams < 50 engineers
│ ┌─────┴────┐ ┌──────┴──────┐ │
│ │ Catalog │ │ Notification│ │
│ │ Module │ │ Module │ │
│ └──────────┘ └─────────────┘ │
└─────────────────┬──────────────┘
│
┌─────────┴────────┐
│ Shared DB │
└──────────────────┘
Trade-offs
Monolith strengths
- Simple local development—no service mesh, no Docker Compose with 12 services
- ACID transactions across all operations without saga orchestration
- Low operational burden: one deployment, one log stream, one database to manage
- Easier to refactor because the entire codebase is visible to your IDE and compiler
Monolith weaknesses
- Deployment coupling: every release ships everything, even unrelated changes
- Scaling is all-or-nothing: you cannot scale only the high-traffic module
- Technology lock-in: the entire system must use the same language and runtime
- Organizational friction: as teams grow, merge conflicts and coordination overhead multiply
Implementation considerations
If choosing a monolith, invest heavily in modular structure from day one. Define explicit module APIs and forbid direct cross-module database access—use the same discipline you would apply if the modules were separate services. Tools like ArchUnit (Java), Dependency Cruiser (JavaScript), or import linters can enforce these boundaries automatically in CI. This discipline pays dividends whether you stay monolithic or eventually extract services.
If choosing microservices, start with the minimum viable number of services—often three to five—based on the clearest domain boundaries. Invest in platform capabilities before writing business logic: service discovery, distributed tracing (OpenTelemetry), centralized logging, and a deployment pipeline per service. The operational cost of microservices scales with the number of services, so resisting service proliferation is critical. The most common failure mode is services that are too small, creating a distributed monolith where services cannot operate independently.
For transitioning from monolith to microservices, the Strangler Fig Pattern provides a safe migration path. Introduce an API gateway in front of the monolith. Extract one service at a time, routing specific endpoints to the new service. The monolith shrinks incrementally rather than being replaced in a risky big-bang rewrite. Domain-Driven Design's bounded contexts provide the conceptual tool for identifying correct service boundaries during the extraction process.
Common mistakes
- Premature decomposition: Splitting into microservices before understanding the domain leads to services with wrong boundaries that must be merged or resplit later at high cost.
- Distributed monolith: Services that share a database or are always deployed together have the complexity of microservices with none of the independence benefits.
- Nanoservices: A service per function or per database table creates enormous coordination overhead; aim for services that align with business capabilities, not technical layers.
- Skipping the modular monolith phase: Going directly from a chaotic monolith to microservices without first establishing clear boundaries guarantees that bad boundaries are replicated across service APIs.
- Ignoring Conway's Law: Building a microservices architecture when a single team owns all services means you pay all the distributed systems costs with none of the team independence benefits.
Decision checklist
- How many engineers will own this system, and are they organized into independent teams?
- How well-understood are the domain boundaries today? Can you draw a clean bounded context map?
- Do different parts of the system have significantly different scaling or availability requirements?
- Does the team have experience operating distributed systems (Kubernetes, service mesh, distributed tracing)?
- Is independent deployment of individual components a hard business requirement?
- What is the cost of a full outage—does fault isolation justify the microservices overhead?
- Have you considered a modular monolith as an intermediate step?
Real-world examples
- Shopify: Ran as a Rails monolith for over a decade, scaling it to handle Black Friday peaks through careful modularization and database sharding rather than decomposing into microservices. Their modular monolith approach allowed them to move fast while maintaining operational simplicity.
- Amazon: Famously decomposed from a monolith into services in the early 2000s, driven by independent team ownership. The two-pizza team rule ensured that each service was owned by a team small enough to work independently. The key was team structure preceding architecture.
- Segment: Rebuilt their microservices architecture back into a monolith after the operational overhead of 140+ microservices owned by a small team became untenable. Published a detailed post-mortem highlighting that microservices were the wrong choice for their team size at the time.
Related decisions
- Synchronous vs Asynchronous Communication — microservices require choosing how services communicate
- Modular Monolith — the recommended middle ground explored in depth
- Microservices Architecture — detailed patterns for implementing microservices
- Distributed Systems — the challenges introduced when choosing microservices