Warming up the neural circuits...
By the end of this chapter you will:
Microservices are not the goal. Shipping working software that your team can maintain is the goal. Sometimes that's a monolith.
A food truck has one cook. She handles orders, grills burgers, fries potatoes, and takes payment. It's efficient because everything is close — one person, one space. That's a monolith. It works beautifully until the line gets long.
A Michelin-starred restaurant has a cold station (salads, desserts), hot station (entrees), pastry station, and expediter (coordinates timing). Each station has specialized chefs, equipment, and ingredients. That's microservices. But here's the catch: open a Michelin restaurant for a food truck's customer volume and you'll go bankrupt. Those specialized chefs cost money, the coordination overhead is massive, and 90% of your resources sit idle.
The same is true for software. A monolith is fast to build, easy to debug, and perfectly adequate for most products. Microservices solve specific problems — team scaling, independent deployment, fault isolation — at a steep cost in complexity. Most teams adopting microservices early aren't solving a problem; they're buying complexity they don't need yet. The right question isn't "monolith or microservices?" It's "what's the simplest architecture that solves today's problems while leaving room for tomorrow's growth?"
A modular monolith is a single deployable application with strong internal boundaries. Think of it as a well-organized monolith that can be split later:
src/
├── modules/
│ ├── users/
│ │ ├── users.controller.ts
│ │ ├── users.service.ts
│ │ ├── users.repository.ts
│ │ └── index.ts // Public API of the module
│ ├── orders/
│ │ ├── orders.controller.ts
│ │ ├── orders.service.ts
│ │ ├── orders.repository.ts
│ │ └── index.ts
│ ├── payments/
│ │ └── ...
│ └── notifications/
│ └── ...
├── shared/
│ ├── database.ts
│ ├── logger.ts
│ └── errors.ts
└── app.ts // Wires modules togetherThe key rule: modules communicate only through their public (index.ts). The orders module calls users.findById() — never SELECT * FROM users directly. This boundary is enforced by convention (and ideally by tooling like ESLint import rules or Nx module boundaries).
// modules/users/index.ts — the ONLY public interface
export { UsersService } from './users.service';
export type { User, CreateUserInput } from './users.types';
// modules/orders/orders.service.ts — consumes the public interface
import { UsersService } from '../users';
If every module respects these boundaries, extracting a module into a separate service later becomes a deployment change, not a rewrite.
Domain-Driven Design's "bounded context" concept is the best heuristic for service boundaries. A bounded context is a part of your system with its own ubiquitous language — its own words, rules, and constraints.
Signals that a module should become its own service:
Different deployment cadence. The payments module changes weekly (new payment methods, compliance updates). The users module changes quarterly. Keeping them in the same deployable means every payment change risks breaking user functionality.
Different scaling requirements. The search module handles 10,000 req/s and needs 12 instances. The admin module handles 100 req/s and needs 1 instance. In a monolith, you scale everything together — wasting resources.
Different team ownership. Team A owns orders. Team B owns inventory. In a monolith, Team A's deploy can break Team B's code. Separate services let teams deploy independently.
Different data storage requirements. analytics needs a columnar database (ClickHouse). users needs a relational database (PostgreSQL). A monolith generally uses one primary database.
Fault isolation. The recommendation engine is experimental and crashes occasionally. In a monolith, its crash takes down the entire app. As a separate service, it degrades gracefully (recommendations disappear, but checkout still works).
The worst outcome: you split your monolith into "microservices," but every service depends on every other service. Service A calls Service B calls Service C calls Service A. A failure in C cascades to A and B. You've kept all the complexity of the monolith and added network latency, overhead, and deployment complexity. This happens when teams split along technical layers instead of business capabilities — "the database service," "the auth service," "the email service." Split by business capability: "orders service," "payments service," "notifications service."
Once you have separate services, they need to talk:
Synchronous (HTTP/gRPC):
// Order service calls Payment service to charge a card
const response = await fetch('https://payments.internal/api/charges', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ amount: 2999
Pro: Simple, familiar, request-response model. Con: Tight coupling — if the payment service is down, the order fails. This is where circuit breakers become essential:
import { CircuitBreaker } from 'opossum';
const breaker = new CircuitBreaker(async (chargeData) => {
const res = await fetch('https://payments.internal/api/charges', {
method: 'POST',
headers
When the circuit opens, the order service can accept the order and payment for later, rather than rejecting the customer entirely.
Asynchronous (message broker): Loose coupling, natural queuing, retry logic. Con: Eventual consistency, harder to debug, requires consumers.
Named after the fig tree that grows around a host tree and gradually replaces it. Applied to software: build new functionality as microservices, gradually replacing monolith functionality piece by piece. A proxy (nginx, API gateway) incrementally redirects traffic. The monolith shrinks until it can be retired. At no point is the entire system rewritten.
Each owns its data. No other service reads or writes that data directly. Shared database = shared schema = coupling at the most fundamental level. You can't change the Users schema without coordinating with the Orders team. The API is the contract; the database schema is an implementation detail.
When an operation spans two services with separate databases, you need consistency:
Solutions:
Saga pattern (eventual consistency): Each step emits an event. If a later step fails, previous steps execute compensating transactions.
Transactional outbox: Write the event to the SAME database as the business data. A separate process publishes events reliably.
Two-Phase Commit (2PC): Rarely used in microservices — blocks both databases, doesn't scale, fails if coordinator crashes.
If you find yourself needing a distributed transaction across services, your service boundaries are probably wrong. Revisit your bounded contexts — the data that needs to be transactionally consistent probably belongs in the same service.
Amazon's transformation from monolith to microservices is the canonical case study. In the early 2000s, Amazon.com was a massive monolithic C++ application (Obidos). Every deploy took weeks. A change to the recommendation algorithm could break checkout.
In 2002, Jeff Bezos issued what became known as the "API Mandate" — an internal memo requiring all teams to:
This mandate forced Amazon to decompose their monolith into hundreds of services. Each team owned their service end-to-end (build, deploy, operate). The "two-pizza team" rule (teams small enough to feed with two pizzas) emerged from this decomposition.
The result: Amazon went from deploying once every few weeks to deploying every 11.7 seconds (by 2015). Independent services meant independent deploy schedules. The recommendation team could update their algorithm without coordinating with the checkout team.
But the migration took YEARS, not months. Amazon didn't rewrite the monolith — they used the Strangler Fig pattern, extracting functionality service by service. The original Obidos monolith still runs some legacy functionality to this day.
The lesson: Amazon's success came from organizational change (the API mandate, two-pizza teams) as much as technical change. Conway's Law states that organizations design systems that mirror their communication structure. Amazon restructured their teams first; the microservices architecture followed naturally.
| Mistake | Why it's wrong | What to do instead |
|---|---|---|
| Starting with microservices before product-market fit | You're solving scaling problems you don't have while creating complexity that slows feature development | Start with a modular monolith. Extract services only when specific pain points emerge. |
| Splitting services by technical layer (auth service, db service, email service) | Creates chatty services that all depend on each other — a distributed monolith with network latency | Split by business capability (orders, payments, shipping). Each service owns its data and logic end-to-end. |
| Sharing a database across services | Couples services at the schema level; one team's migration breaks another team's queries | Each service owns its database. Services communicate via API, never direct DB access. |
| Assuming microservices are "more scalable" by default | A poorly designed distributed system is slower and less reliable than a well-designed monolith | Measure, don't assume. A monolith on decent hardware handles 10K+ req/s. Microservices add network latency (~1-50ms per hop). |
| Not implementing circuit breakers and retries | A single downstream failure cascades through every service that depends on it | Use circuit breakers (opossum). Set timeouts. Implement retries with exponential backoff. Default to graceful degradation. |
| Ignoring the organizational cost | Microservices require DevOps maturity: pipelines per service, distributed tracing, centralized logging, container orchestration | Assess your team's operational capabilities before adopting microservices. A team of 3 with no DevOps experience should not run . |
traceparent headers.traceId in every log line./health (is the process alive?) and /ready (can it serve traffic? DB connected? Dependencies available?). Kubernetes uses these for auto-restart and traffic routing.keepAlive: true and maxSockets.Refactor into a modular monolith: Take a monolithic Express app where controllers directly query the database. Refactor into modules (users, posts, comments) with clear public APIs. Enforce module boundaries with ESLint import/no-restricted-paths.
Implement a circuit breaker: Add the opossum library to an endpoint that calls an external API. Configure a circuit breaker with timeout, error threshold, and fallback. Test by killing the external API and verifying your endpoint returns the fallback response.
Extract a service from the monolith: Choose one module from your modular monolith (e.g., users). Extract it into a separate Express service with its own database. Update the monolith to call the new service via HTTP. Add a circuit breaker to the monolith's calls.
Implement the outbox pattern: In the extracted users service, write user-creation events to an outbox table in the same transaction. Build a worker that polls the outbox and publishes events to RabbitMQ. Verify eventual delivery even when RabbitMQ is temporarily down.
Implement a saga for a distributed transaction: Build an order-placement saga spanning three services (orders, inventory, payments). Each step emits an event; failures trigger compensating transactions. Test the scenario where step 3 fails and verify steps 1 and 2 are compensated.
Strangler Fig migration: Set up an nginx reverse proxy in front of your monolith. Extract one endpoint to a new service. Configure nginx to route that endpoint's traffic to the new service. Monitor both services. Gradually extract more endpoints until the monolith handles zero traffic.
Q: What's the difference between a monolith and microservices? A: A monolith is a single deployable application where all functionality runs in one process. Microservices split functionality across multiple independently deployable services, each owning a specific business capability. Monoliths are simpler to develop and debug; microservices enable independent scaling, deployment, and team ownership.
Q: What is a modular monolith? A: A modular monolith is a single deployable application with strong internal boundaries between modules. Modules communicate through defined interfaces (not direct database access) and could be extracted into separate services later. It gives you the development simplicity of a monolith with the organizational clarity of microservices.
Q: What is Conway's Law? A: "Organizations design systems that mirror their communication structure." If you have 4 teams that don't communicate well, you'll end up with 4 poorly integrated services. The architecture reflects the org chart. To change the architecture, you often need to change the org structure first.
Q: Your company has a monolith that takes 45 minutes to deploy and causes weekly production incidents because one team's change breaks another team's functionality. 200 developers work on it. How do you approach decomposition without a 2-year rewrite? A: Start with the Strangler Fig pattern. Identify the module causing the most deployment friction (most frequent changes, most incidents). Extract it first — it gives the biggest ROI. Put a proxy in front. Route its traffic to the new service. Repeat. Key success factors: (1) Get executive buy-in for a multi-year investment. (2) Don't extract everything — some modules will never change enough to justify extraction. (3) Invest in observability first (distributed tracing, centralized logging). (4) Set a clear metric for success (deploy time < 10 min, incident rate -50%). (5) Accept that some functionality stays in the monolith forever — a "monolith plus services" hybrid is a valid end .
Q: How do you handle a business transaction that spans 3 microservices without distributed transactions? A: Use the Saga pattern. Each service performs its local transaction and publishes an event. If a downstream step fails, the preceding services execute compensating transactions (semantic undo). For example: Order Service → (OrderCreated event) → Inventory Service reserves stock → (InventoryReserved) → Payment Service charges → (PaymentFailed) → Inventory Service releases stock (compensate) → Order Service cancels order (compensate). The system is eventually consistent. For critical flows, use reservation patterns: reserve stock for 15 minutes, release if payment doesn't complete.
Q: You're evaluating whether to extract a service from the monolith. What metrics do you look at, and what's your decision framework? A: Metrics: (1) Deployment frequency — if this module deploys 5× more often than the rest, it's a candidate. (2) Incident correlation — if this module's bugs cause incidents in other modules, isolation helps. (3) Resource usage — if this module needs different scaling (CPU/memory/I/O profile), separate deployment saves cost. (4) Team ownership — if 2+ teams modify this module and constantly conflict, separation reduces coordination overhead. Extract only when at least 2 of these metrics are clearly painful AND the team has operational maturity (CI/CD, monitoring, on-call rotation).
Microservices are an organizational pattern, not a technical silver bullet. They solve problems of team scale, deployment independence, and fault isolation — at the cost of network complexity, eventual consistency, and operational overhead. The modular monolith is the underrated middle ground: strong internal boundaries, single deployment, extractable later. When you do extract, use the Strangler Fig pattern to migrate incrementally without a rewrite. The golden rule: one service owns its data. If you're sharing databases, you don't have microservices — you have a distributed monolith, which is the worst of both worlds. Start monolith, go modular, extract only when the pain of staying together exceeds the pain of splitting apart.
What pattern incrementally replaces monolith functionality with new services behind a proxy? A: Strangler Fig pattern
What's the term for microservices that all depend on each other, combining the worst of both architectures? A: Distributed monolith
What DDD concept is the best heuristic for microservice boundaries? A: Bounded context
True or false: Each microservice should have its own database. A: True — shared databases couple services at the schema level.
What pattern ensures reliable event publication when a database write and event publish must be atomic? A: Transactional outbox pattern
What AWS team structure rule emerged from Amazon's microservices migration? A: Two-pizza teams (teams small enough to feed with two pizzas, ~6-8 people)
Which pattern coordinates a distributed transaction across services using compensating transactions? A: Saga pattern