Frontend Labs
⋯

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.

Generated from real-life-system-design-guide.md · 8 Oct 2026 16:36