What this guide is (and isn’t)
This is a <strong>decision guide</strong>. It explains the real tradeoffs between:
- <strong>Monolith</strong> (one deployable application)
- <strong>Microservices</strong> (many independently deployable services)
- <strong>Event-driven architecture</strong> (services react to events; often combined with microservices)
It’s not just definitions—each section calls out:
- operational complexity
- scaling behavior
- failure modes
- data consistency challenges
- when it becomes a “bad idea”
Quick definitions (with a rookie-friendly mental model)
Monolith
One codebase, one (or a few) deployables.
- Pros: simple, fewer moving parts
- Cons: scaling and change isolation can be harder as it grows
Microservices
Many services with clear boundaries, each deployable independently.
- Pros: independent scaling & deployments
- Cons: distributed systems complexity: networking, retries, observability, data consistency
Event-driven architecture
Components communicate by publishing <strong>events</strong> and consuming them.
- Pros: decoupling in time and place, easier propagation of changes
- Cons: async complexity, ordering issues, eventual consistency, “what happened?” debugging
> Important: “event-driven” is a <strong>communication style</strong>. You can do event-driven with a monolith too, but in practice it’s often used with microservices.
The core tradeoff: coupling vs coordination
The central question is:
- Monolith: you avoid coordination by keeping everything together.
- Microservices: you reduce coupling by splitting, but pay with coordination.
- Event-driven: you reduce coordination in request/response paths by using async events, but you pay with causal reasoning.
A useful framing:
- <strong>Monolith</strong> optimizes for <strong>team speed early</strong>.
- <strong>Microservices</strong> optimize for <strong>organizational speed as teams grow</strong>.
- <strong>Event-driven</strong> optimizes for <strong>system responsiveness & decoupled change</strong>.
Comparison table (high signal)
| Dimension | Monolith | Microservices | Event-driven (often with microservices) |
|---|---|---|---|
| Deployments | Simple | Many pipelines | Many consumers/producers |
| Scaling | Scale whole app | Scale per service | Scale per consumer/workload |
| Complexity | Low | High | Medium→High (debugging + consistency) |
| Failure handling | Fewer network failures | Retries, timeouts, partial failures | Duplicates, out-of-order, eventual consistency |
| Data consistency | Usually ACID | Harder across services | Eventual consistency, sagas |
| Team autonomy | Lower as code grows | High when boundaries are real | High, but requires discipline |
| Observability | Easier | Requires tracing & metrics | Requires correlation + replay tooling |
Monolith (detailed)
How it usually looks
- one repo (maybe modular packages)
- one database (often)
- one deployment artifact
When a monolith is the best choice
- early product / small team
- you want fast iteration
- your biggest bottleneck is feature delivery, not scale isolation
- you can keep strong modular boundaries inside the codebase
How to keep a monolith from becoming a “big ball of mud”
- strict module boundaries
- clear ownership of domains
- do not let modules call each other randomly
- use a single “integration layer” for external systems
Scaling in a monolith
You scale by:
- adding more instances of the whole app
- optimizing queries
- caching
This can become expensive when only one domain needs heavy scaling.
Microservices (detailed)
What “microservices done right” actually means
Microservices aren’t just “many repos”. They require:
- clear ownership boundaries
- independent deployment
- resilient communication patterns
- observability (logs + metrics + traces)
If you don’t have those, microservices become *distributed monoliths*.
When microservices are a good idea
- multiple teams need to ship independently
- you have clear domain boundaries
- traffic is uneven (one area needs scaling without scaling the whole app)
- you can invest in platform capabilities (CI/CD, tracing, service discovery, auth, retries)
Common failure modes
- <strong>Synchronous call chains</strong> (requests that call 5 services in a row)
- <strong>No timeouts / retries</strong> → cascading failures
- <strong>No idempotency</strong> → duplicates break business operations
- <strong>No distributed tracing</strong> → debugging becomes guesswork
For idempotency patterns: see <strong>idempotency-in-distributed-systems</strong>.
Data consistency in microservices
Most real systems end up with:
- per-service databases
- eventual consistency across boundaries
Common approaches:
- outbox pattern
- sagas
- careful read models
Event-driven architecture (detailed)
What it changes
Instead of:
- Service A calls Service B and waits
You do:
- Service A publishes an event like <code>OrderPlaced</code>
- Service B consumes it like “Update inventory”
This shifts the system from request/response coupling to <strong>change propagation</strong>.
When event-driven is the best choice
- you need to react to business events (order placed, user registered)
- you need decoupling between teams/domains
- you want to fan out changes to multiple consumers
- you have background workflows that don’t need immediate synchronous response
What event-driven breaks (or makes hard)
- <strong>Ordering</strong>: events may arrive out of order
- <strong>Duplication</strong>: at-least-once delivery is common
- <strong>Causality</strong>: “why did this happen?” needs correlation
This is where correctness patterns matter:
- <strong>idempotent consumers</strong>
- deduplication keys
- retry policies
- replay tooling
For deeper semantics: see <strong>exactly-once-semantics-myths</strong>.
Data consistency with events
Typical patterns:
- publish events after a successful state change
- use the <strong>outbox</strong> to avoid “DB updated but event lost”
- model changes as a sequence of events
Mono vs Micro vs Event: how to choose (practical decision rules)
Rule 1: Team + release frequency
- One team, releasing monthly? → start with monolith.
- Multiple teams, deploying weekly? → consider microservices.
- Many teams reacting to each other’s changes? → event-driven helps.
Rule 2: Scaling requirements
- Whole app scales together → monolith is efficient.
- Only certain domains scale differently → microservices (or at least isolate those domains).
Rule 3: Latency vs throughput
- Need fast synchronous response to the user → monolith or microservices with careful RPC.
- Can handle async workflows (emails, reports, projections) → events.
Rule 4: Consistency requirements
- Strong consistency everywhere → monolith (or limited distributed consistency).
- Business can tolerate eventual consistency → microservices + events.
Rule 5: Operational maturity
- If you don’t yet have tracing, SLOs, and incident playbooks, microservices will hurt.
Common “hybrid” approach in the real world
Most mature systems look like:
- <strong>Monolith first</strong> (to learn domain and ship)
- <strong>Modular monolith</strong> (organize code internally)
- Extract the hardest domains into <strong>microservices</strong>
- Add <strong>event-driven</strong> flows for background processing and decoupling
This hybrid approach avoids the “big bang rewrite” trap.
Concrete example: e-commerce
Let’s say you have:
- checkout
- inventory updates
- order confirmation email
- analytics projections
Monolith
- one app updates everything in one DB transaction (if needed)
- email/analytics may still be async jobs but within the same deployment
Microservices
- checkout service writes order data
- inventory service updates stock via API call (synchronous) or async
- email service triggers email
Event-driven
- checkout publishes <code>OrderPlaced</code>
- inventory consumes it and updates stock
- email consumes it and sends confirmation
- analytics consumes it and updates read models
Result: decoupled changes, fan-out, fewer synchronous dependencies.
Final guidance: a “no regrets” path
- Start with a <strong>modular monolith</strong>.
- Identify hotspots (slow DB queries, noisy domains, frequent changes).
- Extract <strong>microservices only where boundaries are real</strong>.
- Introduce <strong>events</strong> for domain propagation and async workflows.
- Build correctness primitives early: idempotency, retries, observability, deduplication.