Monolith vs Microservices
Two architectural styles often presented as opposites, but which actually form a continuous spectrum. The choice between them determines how the system is deployed, scaled and operated — with deep consequences for both small and large teams.
Intent
A Monolith is a single deployment unit. All of the application's code is compiled, packaged and deployed together. Microservices are a set of services with well-defined domain boundaries, each with its own deployment cycle, database and independent scalability.
A Monolith isn't synonymous with disorganized code. A monolith can — and should — have clear internal boundaries, well-encapsulated modules and rigorous separation of responsibilities. What defines it is the deployment unit: a single artifact that goes up or down as a whole.
Microservices aren't synonymous with good practices. What defines them is operational independence: each service can be deployed, scaled, updated and can fail without affecting the others. That independence has a concrete cost — network communication, eventual consistency between services, distributed-operations complexity — that only pays off once the system and the organization have reached a size and maturity that justify it.
Problem
The decision is rarely made consciously. The two most common problems appear at opposite extremes:
- The Monolith without internal boundaries (Big Ball of Mud): as the system grows, modules start depending directly on other modules' internal details. What was a clear separation turns into a tangle. Any change can break something elsewhere. The deploy is still a single unit — but the code is so coupled that nobody can work fast without fear.
- Premature microservices: a five-person team with a product still being validated decides to adopt microservices "to scale." The result is that 80% of the time goes into infrastructure (Kubernetes, service mesh, per-service CI/CD, distributed tracing, sagas for transactions) and 20% into the product. Operational complexity outpaces the team's capacity before the product proves it needs to scale.
The central problem is the false dichotomy: as if the only options were "messy monolith" or "sophisticated microservices." There's an entire spectrum between the two extremes, and most systems live comfortably somewhere in between.
Structure
The diagram below contrasts the structure of a monolith with internal modules and a microservices cluster with an API Gateway. Note that a monolith can have domain boundaries just as clear as microservices — the difference is that communication between modules happens in memory, with no network overhead.
MONOLITH (single deployment unit)
┌────────────────────────────────────────────────────┐
│ Application │
│ │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ Module │ │ Module │ │
│ │ Orders │──▶│ Payments │ │
│ └─────────────┘ └─────────────┘ │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ Module │ │ Module │ │
│ │ Inventory │◀──│ Users │ │
│ └─────────────┘ └─────────────┘ │
│ │
│ in-memory communication (direct calls) │
└────────────────────────┬───────────────────────────┘
│ 1 deploy, 1 process
▼
┌───────────┐
│ DB │
└───────────┘
MICROSERVICES (independent services)
┌───────────────┐
│ API Gateway │
└───────┬───────┘
│
┌─────────────────┼─────────────────┐
│ │ │
▼ ▼ ▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Service │ │ Service │ │ Service │
│ Orders │ │ Payments │ │ Users │
└──────┬──────┘ └──────┬──────┘ └──────┬──────┘
│ │ │
▼ ▼ ▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ DB │ │ DB │ │ DB │
│ own │ │ own │ │ own │
└─────────────┘ └─────────────┘ └─────────────┘
communication via HTTP/gRPC/messaging (network)
independent deploy, scaling and failure per service
The spectrum: not a binary choice
Between the two extremes lies a continuum of architectural styles. The progression below isn't a recipe — it's a map of options:
- Monolith: a single deployment unit, with no explicit internal boundaries. Works well for small systems and beginning teams, but deteriorates without discipline.
- Modular Monolith: a single deployment unit with rigorous internal domain boundaries. Modules communicate only through defined public interfaces. Microservices' main benefit (domain separation) without the operational cost.
- SOA / Services: a few large, relatively independent services, frequently sharing a centralized message bus (ESB). A common style in companies with large legacy systems.
- Microservices: many small, independent services, each with its own database and decentralized communication (HTTP, gRPC or direct messaging). Each service can be deployed and scaled fully autonomously.
- Serverless / FaaS: individual functions as the deployment unit. Scales to zero. No server to manage. Operational complexity outsourced to the provider, but with new challenges around cold start, observability and implicit coupling via events.
How it works
Monolith
All calls between modules happen in memory, within the same process. There's no network latency, no partial communication failure, and no need to serialize data between domains. ACID transactions spanning multiple modules are trivial — it's a single database transaction. The deploy is a single artifact: a Docker image, a JAR, a Go binary. Rollback is simple: revert to the previous image.
The cost shows up with growth: without discipline around internal boundaries, modules start depending on each other's implementation details. A single team needs to coordinate changes that affect several parts of the system. Build and test time grows with the size of the codebase. Scaling one specific part means scaling the whole monolith.
Microservices
Each service is an independent process. Communication between services happens over the network — synchronous HTTP, gRPC or asynchronous messaging (Kafka, RabbitMQ). Each service has its own database, so there are no trivial distributed transactions: operations that cross service boundaries need sagas (sequences of local transactions with compensations) or need to accept eventual consistency.
The concrete benefit is operational independence: the Payments team can deploy without coordinating with the Orders team. The Catalog service can scale horizontally without affecting the Checkout service. A failure in the Recommendations service doesn't bring down the Orders service — if the circuit breaker is configured correctly.
The concrete cost is operational complexity: distributed tracing (correlating a request that passed through five services), partial failure handling (what to do when a downstream service doesn't respond), API contract versioning between services, per-service CI/CD, observability infrastructure (centralized logs, metrics, traces) and container orchestration (Kubernetes or equivalent).
The Modular Monolith as a third way
The Modular Monolith captures microservices' main benefit — clear domain separation — without the operational cost of network communication. Modules expose only public interfaces; direct access to another module's internal structures is forbidden by convention or by language mechanisms (private packages, modules with controlled exports). Communication between modules can be synchronous (method calls through an interface) or asynchronous (in-memory events via an internal bus).
Recommended evolutionary path: start with a well-structured modular monolith. When a specific module needs independent scaling, an autonomous deploy cycle, or a different technology — and that pain becomes measurable — extract that module as a service. Don't design microservices before you have the problems they solve.
When to use each approach
Monolith (including Modular Monolith)
- New or still-being-validated products: iteration speed is critical. A monolith reduces friction in deployment, debugging and refactoring. Changing domain boundaries — common in the first few months — is trivial inside a monolith and expensive across services.
- Small team or without maturity in distributed operations: operating microservices requires knowledge of container orchestration, service mesh, distributed tracing and eventual-consistency strategies. A team without that maturity will pay too high a learning cost at the wrong time.
- Domain not yet well understood: service boundaries in microservices are hard to change once established — every boundary change implies data migration, API contract changes and coordination across teams. If the domain is still being discovered, monolith first.
- When ACID consistency is frequently needed: if multiple domain entities frequently need to change atomically, the monolith's local transaction is far simpler than a distributed saga.
Microservices
- Real asymmetric scale: when different parts of the system have radically different loads and scaling the entire monolith to handle one part's peak is economically unfeasible.
- Independent, autonomous teams: Conway's Law is relentless — the system's architecture mirrors the organization's communication structure. Microservices make sense when there are autonomous teams, with clear domain owners, who need to deploy independently without constant coordination.
- Radically different technologies or requirements per domain: an image-processing service in Python, a low-latency service in Go and a reporting service on the JVM can coexist when each is an independent service.
- An organization already mature in DevOps and distributed operations: per-service CI/CD, observability, distributed incident management and an already-established on-call culture.
Pros and cons
Monolith — Pros
- Simple deploy: a single artifact, trivial rollback, no coordination between services.
- Direct debugging: a complete stack trace in a single process, no need to correlate logs across several services.
- Natural ACID transactions: consistency across multiple entities without sagas or compensations.
- Zero latency between modules: in-memory communication, no serialization or network overhead.
- Less infrastructure: doesn't require a container orchestrator, service mesh or distributed tracing.
- Cheaper boundary refactoring: moving code between modules is far simpler than migrating between services.
Monolith — Cons
- All-or-nothing scaling: you can't scale just one part without scaling the entire system.
- Insidious coupling: without active discipline, modules become coupled over time and the system deteriorates.
- Build and deploy of the whole system on every change: a small change requires redeploying everything.
- Adoption of new technologies affects the whole system: swapping the database or the framework affects everything at once.
Microservices — Pros
- Independent per-service deploy: autonomous teams can ship without coordinating with other teams.
- Granular scale: scale only the service under load, with infrastructure matched to that load.
- Failure isolation: a failure in one service doesn't have to bring down the whole system (with circuit breakers and graceful degradation).
- Technological flexibility per service: each service can use the language, database and framework best suited to its domain.
- Explicit domain boundaries enforced by the network: the cost of communication discourages accidental coupling.
Microservices — Cons
- High operational complexity: Kubernetes, service mesh, per-service CI/CD, distributed tracing, per-service secrets management.
- Eventual consistency across services: distributed transactions require sagas, with compensation logic and tolerance for partial failures.
- Latency and network failures: calls between services can fail, time out or introduce unpredictable latency.
- API contract versioning: interface changes need backward compatibility or version coordination across teams.
- Observability overhead: correlating a request that passed through five services requires well-configured distributed tracing.
- Infrastructure cost: more processes, more containers, more network and storage resources.
Common pitfalls
1. Big Ball of Mud — the problem isn't size, it's lack of boundaries
A monolith deteriorates not because it grew, but because it grew without disciplined internal boundaries. Modules start directly accessing other modules' database tables, calling other classes' internal methods, and sharing global state. The result is a system where nobody knows the full impact of a change. The solution isn't migrating to microservices — it's enforcing boundaries. A Big Ball of Mud migrated to microservices becomes a Distributed Monolith (see below), which is even worse.
2. Distributed Monolith — the worst of both worlds
The Distributed Monolith happens when multiple services are deployed independently, but are so coupled that they can't operate independently: services that share the same database, that need to be updated together on every schema change, that call each other synchronously in a long chain with no fault tolerance, or that require coordinated deploys because they don't maintain backward compatibility.
Practical definition: if you need to deploy two or more services at the same time for a feature to work, you have a Distributed Monolith at that point. You pay the operational cost of microservices without gaining the independence that would justify it.
3. The false promise of early speed with microservices
Small teams that adopt microservices at the start of a product frequently report spending more time dealing with infrastructure than with the product itself. Setting up per-service CI/CD pipelines, Kubernetes, service mesh, tracing and distributed configuration management is real work that consumes product time. For a team without a dedicated platform engineer, that cost outweighs any scalability gain the product doesn't need yet.
"You need to be Netflix to need Netflix's microservices before you have Netflix's problem." Netflix migrated from a monolith to microservices after already having millions of users and an organization with hundreds of engineers — not on day zero.
4. Wrong service granularity
Overly granular services (one service per database entity, for example) eliminate any local transaction and multiply saga complexity. Overly large services (one service per technical layer, like a "database service" and a "business service") are just a distributed monolith. The correct boundary for a service is the Domain-Driven Design bounded context: a cohesive business domain with a consistent internal model, not a technical or object-granularity division.
5. Ignoring Conway's Law
Conway's Law states that organizations tend to produce systems that mirror their communication structure. Microservices work when there is a team clearly responsible for each service, with autonomy to make technology and deploy decisions. Trying to adopt microservices without reorganizing the teams — keeping a single team responsible for every service — produces the cost of both worlds with the benefit of neither.
Related architectures and patterns
Monorepo vs Multi-repo is frequently confused with Monolith vs Microservices, but they're orthogonal dimensions: a monorepo can contain multiple independent microservices; a multi-repo can contain the modules of a single monolith. The repository decision affects the development workflow and code reuse, not the deployment topology.
CQRS with separate databases is an especially valuable pattern within microservices: each service keeps its own Read Model designed for its own needs, avoiding cross-service joins over the network. Synchronization happens via events published on the inter-service bus.
Event-Driven Architecture is the preferred asynchronous communication mechanism between microservices. Instead of synchronous HTTP calls chained together (which create temporal coupling and amplify failures), services publish domain events that other services consume independently. EDA doesn't eliminate the complexity of eventual consistency, but it makes it explicit and manageable.
Hexagonal Architecture and Layered Architecture describe the internal organization of each service (or of the monolith). They're complementary to the Monolith vs Microservices decision: a microservice well structured internally with Hexagonal has clear ports that make extracting or merging services easier in the future.