Microservices vs Monolith
A team of 5 engineers building an early-stage product does not need microservices. A single deployable unit with a well-structured codebase is faster to build, simpler to debug, and cheaper to operate. Microservices become justified when the organization scales — multiple teams, independent release cadences, and workloads with vastly different scaling profiles. The architecture choice is driven by team structure and operational maturity, not by technology hype.
Monolith
A monolith is a single deployable unit where all functionality lives in one codebase, one build artifact, and one process (or a few replicas of it).
flowchart TB
Client([Client]) --> LB[Load Balancer]
LB --> M1[Monolith Instance 1]
LB --> M2[Monolith Instance 2]
LB --> M3[Monolith Instance 3]
subgraph Monolith Instance
direction TB
API[API Layer]
API --> Auth[Auth Module]
API --> Orders[Order Module]
API --> Payments[Payment Module]
API --> Inventory[Inventory Module]
Orders --> DB[(Single Database)]
Payments --> DB
Inventory --> DB
Auth --> DB
endBenefits
| Benefit | Why |
|---|---|
| Simple deployment | One artifact to build, test, and deploy. No service orchestration, no version matrix. |
| Easy cross-module calls | In-process function calls — no network latency, no serialization, no service discovery. |
| ACID transactions | All modules share one database. A single transaction can atomically update orders, inventory, and payments. |
| Simple debugging | One process — stack traces span the full request path. No distributed tracing needed. |
| Lower operational cost | One build pipeline, one monitoring stack, one deployment target. |
When It Breaks Down
| Symptom | Root Cause |
|---|---|
| Deploy fear | A change to the payment module requires deploying the entire application — including untested order changes a colleague committed. |
| Scaling mismatch | The image processing module needs 10× the CPU of the checkout module, but you can only scale the whole monolith. |
| Team contention | 6 teams merge code into one repository. Merge conflicts, broken builds, and coordination overhead slow everyone. |
| Long build/test times | The full test suite takes 45 minutes. Engineers avoid running it, and broken code reaches production. |
Microservices
Each business capability becomes an independently deployable service with its own codebase, database, and team.
flowchart TB
Client([Client]) --> GW[API Gateway]
GW --> Auth[Auth Service]
GW --> Orders[Order Service]
GW --> Payments[Payment Service]
GW --> Inventory[Inventory Service]
Auth --> AuthDB[(Auth DB)]
Orders --> OrderDB[(Order DB)]
Payments --> PayDB[(Payment DB)]
Inventory --> InvDB[(Inventory DB)]
Orders -->|async event| Payments
Orders -->|async event| InventoryBenefits
| Benefit | Why |
|---|---|
| Independent deployment | Deploying the payment service doesn’t touch orders. Each service has its own release cadence. |
| Team autonomy | The payments team owns their service end-to-end — code, data, deployment, monitoring. No cross-team merge conflicts. |
| Isolated failure domains | If the recommendation service crashes, checkout still works. |
| Per-service scaling | Scale the image processing service to 50 replicas while the user profile service runs on 3. |
| Technology heterogeneity | The search service can use Elasticsearch + Go while the order service uses PostgreSQL + Java. |
The Cost
| Cost | Detail |
|---|---|
| Network overhead | Every cross-service call is an RPC — serialization, latency, failure handling. A local function call becomes a distributed system problem. |
| Data consistency | No shared transactions. Cross-service consistency requires Saga pattern or eventual consistency. |
| Operational complexity | N services = N build pipelines, N monitoring dashboards, N deployment configurations. Requires container orchestration (Kubernetes), service discovery, distributed tracing. |
| Debugging difficulty | A request touches 5 services. Finding the root cause requires distributed tracing (Jaeger, Zipkin), correlated logs, and service dependency maps. |
The Distributed Monolith Anti-Pattern
The worst of both worlds: you split into services but they’re tightly coupled — shared databases, synchronous call chains, coordinated deployments.
flowchart LR
A[Order Service] -->|sync HTTP| B[Payment Service]
B -->|sync HTTP| C[Inventory Service]
C -->|sync HTTP| D[Shipping Service]
A --> DB[(Shared Database)]
B --> DB
C --> DB
D --> DBSymptoms:
- Deploying one service requires deploying others (they share schema)
- A single slow service cascades failures through the synchronous chain
- Teams can’t make schema changes without coordinating with every other team
- You have the operational cost of microservices with none of the benefits
Root cause: decomposition was done by technical layer (API layer, business logic layer, data layer) instead of business capability (order management, payments, inventory).
Decomposition Strategies
By Business Capability
Each service maps to a business function. The service owns the full vertical slice — API, logic, and data.
E-commerce platform:
├── Order Service (create order, order status, order history)
├── Payment Service (charge, refund, payment methods)
├── Inventory Service (stock levels, reservations, replenishment)
├── Catalog Service (products, pricing, categories)
├── User Service (registration, profiles, authentication)
└── Notification Service (email, push, SMS)By DDD Bounded Context
Start with Domain-Driven Design: identify bounded contexts where terms have unambiguous meaning. An “Order” in the order context and a “Shipment” in the shipping context may refer to the same real-world event but have different data models and lifecycles.
Bounded Context: Order Management
- Aggregate: Order (id, items, status, total)
- Events: OrderPlaced, OrderCancelled
- Own database, own team
Bounded Context: Shipping
- Aggregate: Shipment (id, tracking_number, carrier, status)
- Events: ShipmentCreated, ShipmentDelivered
- Own database, own team
Communication: Order Management publishes OrderPlaced →
Shipping consumes it and creates a ShipmentDecision Framework
| Factor | Choose Monolith | Choose Microservices |
|---|---|---|
| Team size | < 10 engineers | > 20 engineers, multiple teams |
| Product maturity | Early-stage, MVP, unclear domain boundaries | Established product with well-understood domains |
| Operational maturity | No DevOps team, no Kubernetes, no distributed tracing | Infrastructure automation, CI/CD, container orchestration |
| Scaling needs | Uniform — all modules scale the same | Divergent — some modules need 10× the resources of others |
| Deployment cadence | Weekly or less frequent | Multiple times per day, per-team |
| Data consistency needs | Strong ACID transactions across domains | Eventual consistency acceptable via Sagas |
| Domain boundaries | Unclear — still discovering what the product is | Clear — teams own distinct business capabilities |
“Start monolith, extract services when clear boundary emerges” is the pragmatic default. Premature decomposition into microservices creates a distributed monolith — you pay for the complexity but get none of the benefits. The services you extract first should be the ones with the clearest boundary: a service that has a distinct team, a distinct data model, and a distinct scaling requirement.
The Modular Monolith Alternative
A middle ground: structure the monolith into well-encapsulated modules with strict boundaries — separate packages, no cross-module database queries, communication only through defined interfaces. When a module needs to become a service, it can be extracted with minimal refactoring.
monolith/
├── modules/
│ ├── orders/
│ │ ├── api/ (public API — other modules call this)
│ │ ├── internal/ (private — no other module imports this)
│ │ └── schema/ (own tables — other modules can't query directly)
│ ├── payments/
│ │ ├── api/
│ │ ├── internal/
│ │ └── schema/
│ └── inventory/
│ ├── api/
│ ├── internal/
│ └── schema/
└── shared/
└── events/ (event bus for async communication between modules)Shopify ran this pattern at enormous scale — a modular Rails monolith serving millions of merchants — before selectively extracting critical services.
Interview tip: When asked “monolith or microservices?” don’t default to microservices. Say: “For an early-stage system with a small team, I’d start with a modular monolith — strict module boundaries, separate schemas per module, inter-module communication through events. This gives us the option to extract services later without premature complexity. The first extraction candidate would be the module with the most distinct scaling requirement — say, the image processing pipeline — because it needs GPU workers while the rest of the system is CPU-bound.” This shows you understand when and why to decompose, not just how.
Test Your Understanding
A team splits a monolith into 8 microservices. Every service calls 3-4 others synchronously. A change in the Order service requires coordinated deploys of 5 services. What went wrong?
Distributed monolith — the worst of both worlds. Services are split by code boundary but remain tightly coupled via synchronous calls and shared deployment schedules. You pay the operational cost of microservices (network, observability, deployment complexity) without the benefits (independent deployability, isolated failure domains).
Fix: Services should communicate via async events (Kafka), not synchronous HTTP chains. Each service owns its data and makes decisions based on local state + consumed events. If 5 services must deploy together, the boundaries are wrong — recombine them or redraw boundaries along business capabilities with clear data ownership.
When should you extract the first microservice from a monolith? Give a concrete signal.
When one module has a distinctly different scaling or deployment requirement. Concrete signals:
- Scaling mismatch: The image processing module needs GPU workers while the rest is CPU-bound. Scaling the monolith wastes GPU resources on the API layer.
- Deploy cadence mismatch: The payment module changes quarterly (compliance), the feed algorithm changes daily. Deploying the monolith for a feed tweak risks the payment path.
- Technology mismatch: The ML ranking module needs Python; the rest is Java. Running both in one process is painful.
Don’t extract based on: “this code is complex” (refactor the module boundary first), “microservices are best practice” (they’re not universal), or “to reduce team coupling” (Conway’s Law means the team structure must change too).