Microservices

When to split a system, and when splitting is the wrong call. The costs are real and mostly organizational — start with a monolith.

Monolith first What they buy What they cost Boundaries Strangler fig
🧱

Monolith First

The right default is a well-structured monolith: one deployable unit with clean internal modules. It's not the primitive option — it's the one that keeps the things microservices throw away. Calls between modules are local function calls (fast, no partial failure). A change spanning two areas is one atomic database transaction. There's one thing to deploy, one place to debug, one log to read.

Most systems don't die from lack of scale. They die from complexity. A monolith you can reason about beats a distributed system you can't.

💡
The senior move is a modular monolith: enforce clean module boundaries inside one deployable, so the seams are ready if you ever need to split. You get organizational clarity now and cheap extraction later, without paying the distributed-systems tax before you have to.
💳

What Microservices Actually Buy

Microservices deliver real benefits — just fewer, and later, than the hype suggests.

BenefitWhat it means
Independent deploysShip one service without redeploying the whole system
Independent scalingScale the hot service alone instead of the entire app
Team autonomyA team owns a service end to end, chooses its own stack and pace
Fault isolationOne service crashing needn't take the others down (with the right patterns)
ℹ️
The honest driver is usually organizational, not technical. This is Conway's law — systems mirror the communication structure of the org that builds them. Microservices let many teams ship without stepping on each other. A five-person team doesn't have that problem, so it doesn't need that solution. If the reason to split is "so teams stop blocking each other," say so; that's more convincing than a vague scaling claim.
💸

What They Cost

Splitting turns in-process certainties into distributed problems. The costs are concrete, and they show up on day one, not at scale.

  MONOLITH                              MICROSERVICES
  order.charge(card)                    POST http://payment/charge
   ├─ function call: ~nanoseconds        ├─ network call: ~milliseconds
   ├─ either returns or throws           ├─ may time out, retry, half-fail
   └─ one DB transaction wraps it all    └─ no shared transaction (saga needed)
CostBites you as
Network everywhereLatency, timeouts, partial failure on every call (Resilience)
Distributed transactionsNo cross-service ACID — sagas and compensation (Transactions)
Operational surfaceMany deploys, dashboards, versioned contracts, tracing (Telemetry)
Local dev painRunning twelve services to test one feature
⚠️
The trap is a distributed monolith: services split apart but so chatty and tightly coupled that you pay every cost of distribution and get none of the independence. If deploying service A always requires deploying B, you built the worst of both. The boundaries were wrong.
📐

Drawing Boundaries

Split around business capabilities — checkout, search, notifications — not around database entities. A capability owns its data and exposes behavior; an entity-shaped service ("the User service") ends up called by everyone for everything and becomes a shared bottleneck.

The load-bearing rule is database-per-service. Each service owns its data and no other service touches that database directly — access goes through the API. Share a database and you've recoupled everything: a schema change in one service breaks another, and you're back to a monolith with network calls in the middle. Getting data across boundaries then happens three ways: synchronous API calls, events for async propagation (Messaging), or maintaining a local read replica of another service's data via its event stream.

  RIGHT: each service owns its store        WRONG: shared database
  ┌─────────┐  ┌─────────┐  ┌─────────┐     ┌─────────┐ ┌─────────┐
  │ orders  │  │ payment │  │ search  │     │ orders  │ │ payment │
  │  ↓ own  │  │  ↓ own  │  │  ↓ own  │     └────┬────┘ └────┬────┘
  │  DB     │  │  DB     │  │  DB     │          └────┬──────┘
  └─────────┘  └─────────┘  └─────────┘          ┌────┴────┐
   cross-service data via API / events           │ shared DB│ ← recouples both:
                                                  └─────────┘   a schema change
                                                                breaks everyone
🌱

Decomposition Path

You don't rewrite a monolith into microservices in one heroic push — that's how projects die. The strangler fig pattern extracts incrementally: route one capability at a time out to a new service, leave the rest in the monolith, and repeat. A routing layer sends the extracted paths to the new service and everything else to the old code, so the system keeps running throughout.

⚠️
Big-bang rewrites fail because you freeze the old system, build the new one for a year, and discover at cutover that the requirements moved and the edge cases were undocumented. Extracting one route at a time keeps every step small, reversible, and shippable. Never propose "we'll rewrite it as microservices" as a plan.
🎤

In the Interview

Reaching for microservices unprompted reads as pattern-matching, not judgment. The senior answer names the tradeoff and defers the split until a requirement forces it.

💡
The line to have ready: "I'd start with a modular monolith and split X out when Y." For example: "one service until the payment flow needs its own scaling and an independent deploy cadence, then I'd extract payments first because it has the clearest boundary and the strictest reliability needs." That shows you know microservices and that you won't reach for them before there's a reason (Tradeoffs).
📋

Quick Reference

MonolithMicroservices
CallsLocal, fast, reliableNetwork — latency, partial failure
TransactionsReal ACID across the appSagas, eventual consistency
DeployOne unitIndependent per service
ScalingWhole app togetherPer service
TeamsCoordinate on one codebaseAutonomous per service
Ops burdenLowHigh — many moving parts
Signals you're ready to split
Teams block each other on one codebase / deploy pipeline
One component needs radically different scaling than the rest
Parts have conflicting reliability or tech requirements
The monolith's build/test/deploy cycle has become the bottleneck
ℹ️
The one-liners to keep ready. Start with a modular monolith; most systems die of complexity, not scale. Microservices buy independent deploy, scaling, and team autonomy — the real driver is usually organizational (Conway's law). They cost network calls, distributed transactions, and ops surface. Split around business capabilities with a database per service; never share a database. Extract incrementally with the strangler fig, never a big-bang rewrite.