TL;DR
- Start with a modular monolith: one deployable app with clear internal boundaries (modules/packages). It’s fastest to ship and simplest to operate.
- Move to microservices when you have team scale, clear bounded contexts, and a real need for independent deploys/scaling. Otherwise you trade code complexity for ops complexity.
- Microservices require platform maturity (CI/CD, service discovery, observability, gateways, messaging) and disciplined data ownership (DB‑per‑service).
- Evolve incrementally with a strangler approach: carve off one capability at a time behind stable interfaces.
Mental model (60 seconds)
Monolith: [ Web + API + Jobs + DB ] (single codebase & deploy)
Modular Monolith: [ Web | API | Jobs ] (modules) → [ DB ]
Microservices: [ Web ] [ Orders ] [ Billing ] [ Search ] ... (many services)
│ │ │
(gateway) (events) (DB‑per‑service)
- Monolith: one unit to build, test, deploy. Simple debugging; easy transactions. Can get big, but still manageable with modules.
- Microservices: many small, independently deployable services talking over the network (sync/async). Enables team autonomy, but adds latency, retries, versioning, and operational overhead.
Quick comparison
| Aspect | Monolith / Modular Monolith | Microservices | |---|---|---| | Deploy unit | One | Many (per service) | | Scaling | Whole app or by process type | Per service (right‑size resources) | | Consistency | In‑process, ACID easy | Often eventual, needs sagas/outbox | | Latency | In‑process calls (fast) | Network hops; need timeouts/retries | | Testing | Simple end‑to‑end | Contract tests, test envs, mocks | | Failure modes | Process failure | Partial failures, cascading risk | | Team autonomy | Shared repo/process | Independent releases per team | | Local dev | Run app + DB | Orchestrate many deps; use mocks/dev‑containers | | Ops cost | Lower (fewer moving parts) | Higher (observability, infra, oncall) |
When a monolith is the better choice
- Small team or new product where speed trumps everything.
- Domain still changing; you need cross‑cutting refactors without network boundaries.
- Heavy transactional workflows (multi‑object ACID).
- Limited ops budget or platform maturity.
Make it modular: define packages/modules per bounded context (e.g., Orders, Billing, Catalog). Keep clear interfaces even inside one repo so extraction later is natural.
When microservices pay off
- Multiple teams stepping on each other in one codebase; need independent deploy cadence.
- Uneven scaling (e.g., search or media processing dwarf everything else).
- Reliability isolation: a bad feature shouldn’t take the site down.
- Compliance/tenancy boundaries (data separation, geo).
Prereqs:
- Automated CI/CD, service discovery, centralized config, and observability (logs/metrics/traces).
- API gateway for auth, rate limiting, CORS, and routing.
- Messaging (events/queues) and patterns like Outbox, Idempotency, Sagas.
Data & integration
- Database‑per‑service. Avoid shared tables across services. Share events/APIs, not schemas.
- Prefer async integration for cross‑service workflows: publish domain events (
OrderCreated) and react elsewhere. - For multi‑step consistency use Sagas (orchestration or choreography) + Outbox to emit events atomically with state changes.
Communication patterns (picks that age well)
- External clients → API Gateway → services (REST/gRPC).
- Service↔Service:
- Sync: gRPC/HTTP with timeouts, retries, circuit breakers.
- Async: Kafka/SNS/SQS/NATS; consumers must be idempotent.
- Don’t be chatty: design coarse‑grained APIs to avoid N‑shot network waterfalls.
Migration playbook (strangler pattern)
- Draw boundaries inside your monolith (modules / contexts).
- Put a stable interface in front of the module (internal API).
- Extract one module behind the same interface (proxy calls to the new service).
- Move data ownership (new DB), publish events; leave anti‑corruption adapters where needed.
- Repeat. Track latency, error budgets, and oncall load to ensure it’s worth it.
Pitfalls & fixes
| Pitfall | Why it hurts | Fix | |---|---|---| | Distributed monolith (tight coupling across services) | Change still touches many repos | Strong boundaries; fewer, larger services; versioned contracts | | Too many tiny services | Ops overhead explodes | Start with modular monolith → extract only hot contexts | | Sync chains on the request path | High tail latency, failure cascades | Use async events, caches, bulkheads, timeouts | | Shared DB across services | Hidden coupling, lock contention | DB‑per‑service; read models for cross‑views | | Inconsistent auth/observability | Debugging hell | Gateway + correlation IDs + centralized tracing | | No contract tests | Runtime breakage | Add consumer‑driven contracts (Pact, etc.) |
Tiny examples
API gateway route → monolith (today), microservice (tomorrow)
location /api/orders/ {
proxy_pass http://monolith:4000; # later: http://orders:4001
proxy_set_header X-Request-Id $request_id;
}
Outbox (pattern sketch)
-- within the same DB tx as the state change
INSERT INTO orders(id, status, ...);
INSERT INTO outbox(event_type, payload, created_at) VALUES ('OrderCreated', '{"id":"..."}', now());
-- background worker relays outbox → Kafka/SNS (idempotent)
Decision guide (fast)
- Single team, fast changes, strong transactions? → Modular monolith.
- Multiple teams, uneven scale, need isolation? → Microservices (with platform in place).
- Unclear boundaries? → Stay monolith; refine domain first.
Quick checklist
- [ ] Start modular: bounded contexts as packages/modules.
- [ ] Enforce internal interfaces; no leaking across modules.
- [ ] Add observability early (logs/metrics/traces, request IDs).
- [ ] If going microservices: DB‑per‑service, gateway, messaging, timeouts/retries, circuit breakers.
- [ ] Prefer async for cross‑service workflows; ensure idempotency.
- [ ] Migrate with strangler slices; measure latency, cost, and oncall impact.
One‑minute adoption plan
- Carve your monolith into clean modules with interfaces.
- Add request IDs and distributed tracing even in the monolith.
- Extract one high‑value module behind a gateway route; keep behavior identical.
- Introduce events/outbox for cross‑module workflows.
- Standardize service templates (health checks, metrics, logging) before extracting more.