13. Service Boundaries: Monolith, Microservices, and Conway's Law
13.1 The spectrum
Monolith ──▶ Modular monolith ──▶ Macro-services ──▶ Microservices ──▶ Functions
(one deploy) (enforced internal (a few large, (many small, (per-handler)
module boundaries) team-sized) independently
deployable)
Microservices solve an organizational problem, letting many teams deploy independently, and in exchange they create technical ones: network failures, distributed transactions, observability, versioning, and duplicated infrastructure. A strong senior candidate says this out loud and recommends a modular monolith until team count, deploy contention, or very different scaling profiles force a split.
13.2 Finding boundaries
- Domain-driven design: bounded contexts. "Product" means different things to Catalog, Pricing, and Shipping, and each context owns its own model.
- Split by rate of change and scaling profile: e.g. the search indexer vs the checkout API.
- Split by data ownership: each service owns its data; nobody else reads its tables directly. Shared databases are the most common way "microservices" turn back into a distributed monolith.
- Conway's Law: a system's architecture mirrors the org's communication structure. Use it deliberately (the inverse Conway maneuver): design the teams you want the architecture to have.
13.3 Patterns at the edges
| Pattern | Purpose |
|---|---|
| API gateway | One entry point: auth, routing, rate limiting, request aggregation |
| BFF (Backend for Frontend) | One aggregation layer per client type (mobile, web, partner) |
| Service mesh (Istio, Linkerd) | mTLS, retries, timeouts, traffic shifting, telemetry without code changes |
| Anti-corruption layer | Translate a legacy or third-party model at the boundary |
| Strangler fig | Incrementally replace a legacy system behind a facade (§29) |
| Sidecar | Out-of-process helpers (proxy, log shipper) next to each service instance |
13.4 🏗️ Architecture: a modular monolith, and extracting a service from it
┌──────────────────────── Monolith (one deploy) ────────────────────────┐
│ ┌─────────┐ public API ┌─────────┐ public API ┌──────────┐ │
│ │ Catalog │◀──────────────▶│ Orders │◀──────────────▶│ Payments │ │
│ │ module │ │ module │ │ module │ │
│ └────┬────┘ └────┬────┘ └────┬─────┘ │
│ own tables only own tables only own tables only │
│ (boundary enforced by linter: no cross-module imports / joins) │
└───────────────────────────────────────────────────────────────────────┘
Extraction (when Payments needs independent scaling / PCI isolation / separate team cadence):
1. Module already talks via interface → swap in-process call for RPC client
2. Its tables already private → move them to a separate DB (CDC sync during cutover)
3. Route traffic gradually (flag), keep the in-process path as rollback
13.5 🌍 In production
- Shopify: one of the largest Rails monoliths, deliberately kept modular. They "componentized" it and built Packwerk to enforce boundaries statically ("Deconstructing the Monolith", 2019).
- Amazon: the canonical microservices story. The service-oriented mandate and two-pizza teams came from an organizational need for autonomy at scale.
- Segment, "Goodbye Microservices" (2018): about 140 destination services, each with its own queue, became an operational burden. They merged them back into a single service and gained productivity.
- Amazon Prime Video (2023): their stream-quality monitoring tool moved from serverless step functions plus microservices to a single process and reported about 90% lower cost. The point isn't "monoliths good", it's match the architecture to the workload.
- Uber DOMA (Domain-Oriented Microservice Architecture, 2020): after growing to thousands of microservices, Uber grouped them into domains behind gateways, with layers and extension points. Microservices at scale need macro-structure.
13.6 ⚠️ Corner cases & deep details
- Distributed monolith: services that must deploy in lockstep, share a database, or call each other synchronously in long chains. You pay the microservice costs without the benefits. Test: can each team deploy independently, any time of day?
- Shared libraries as hidden coupling: a shared "common" library with business logic forces coordinated upgrades across 50 services. Share infrastructure libraries, not domain logic.
- Chatty boundaries: a page renders with 40 RPCs to the same service, which
means the boundary is wrong, or you need a batch API (
GetUsers(ids[])) or an aggregation layer. - Reporting across services: "revenue by product category" now spans 3 databases. Feed a warehouse via CDC (§21); never join across service databases directly.
- Contract drift: without consumer-driven contract tests (Pact) or schema checks in CI, a field rename breaks a downstream team in production.
- Per-service platform tax: every service needs CI/CD, dashboards, alerts, on-call, security patching, and capacity planning. 200 services × that tax is a significant headcount line, which is an EM and Staff+ concern.