Microservices
Service boundaries, gRPC, distributed transactions.
Part 33: Microservices
Learning Objectives
You will understand why microservices exist, when they are appropriate, and the hard problems they introduce. Most importantly, you will learn when not to use them.
Why Does This Exist?
Imagine you work at a company with 50 engineers all shipping to a single codebase. Every deployment risks breaking everything. Scaling the payments system means scaling everything — including the parts that don't need it. A bug in the recommendation service takes down the checkout page.
Microservices exist to solve organizational and operational scale problems. The key word is scale. If you don't have these problems yet, microservices are an expensive solution to problems you don't have.
Service Boundaries (The Hard Part)
The most critical decision in microservices is where to draw boundaries. The right mental model: each service should represent a bounded context — a domain with clear, stable ownership.
WRONG: Technical layers as services ├── database-service ├── api-service └── frontend-service ✗ These are layers, not domains. Every feature change touches all three. RIGHT: Domain boundaries as services ├── user-service (identity, auth, profiles) ├── product-service (catalog, inventory, pricing) ├── order-service (cart, checkout, order lifecycle) ├── payment-service (charges, refunds, billing) └── notification-service (email, push, SMS) ✓ Each feature change is localized to one service.
Service Communication
SYNCHRONOUS (request-response) ┌──────────────┬──────────────────────────────────────────┐ │ REST/HTTP │ Simple, browser-native, widely understood │ │ gRPC │ Typed contracts, streaming, high perf │ └──────────────┴──────────────────────────────────────────┘ When to use: Real-time queries where you need an immediate answer. Problem: Creates tight temporal coupling. If B is down, A fails. ASYNCHRONOUS (event-driven) ┌──────────────┬──────────────────────────────────────────┐ │ Message queue│ Kafka, RabbitMQ, SQS │ │ Event bus │ Publish events, multiple consumers │ └──────────────┴──────────────────────────────────────────┘ When to use: Actions that don't need immediate results. Benefits: Services are decoupled. Producer doesn't care if consumer is slow. Example: Order placed → order-service publishes "order.created" event payment-service: subscribes → charges the card notification-service: subscribes → sends confirmation email inventory-service: subscribes → decrements stock
Data Ownership
Each service must own its own database. This is non-negotiable. If two services share a database, they are not truly independent — a schema change in one breaks the other.
✗ WRONG: Shared database
order-service ──┐
├── single_database
payment-service─┘
✓ RIGHT: Database per service
order-service → orders_db
payment-service → payments_db
user-service → users_db
Cross-service data needs:
Option 1: API call (synchronous)
Option 2: Event-driven data replication
Option 3: Store a reference (userId) and query when needed
Distributed Transactions (The Hard Truth)
ACID transactions don't cross service boundaries. If order-service creates an order and payment-service charges the card, what happens if the charge succeeds but the order creation fails?
The Saga Pattern: Each step has a compensating transaction (rollback action). Order saga: 1. Reserve inventory ← compensate: release inventory 2. Charge payment ← compensate: refund payment 3. Create order ← compensate: cancel order 4. Notify customer ← no compensation needed If step 3 fails: → Run compensation for step 2 (refund payment) → Run compensation for step 1 (release inventory) This is eventual consistency. It is more complex but it is the reality of distributed systems.
The Modular Monolith: The Better Default
Before reaching for microservices, consider the modular monolith: src/ ├── modules/ │ ├── users/ │ │ ├── users.service.ts ← business logic │ │ ├── users.repository.ts ← data access │ │ ├── users.controller.ts ← HTTP layer │ │ └── users.types.ts ← types (no leaking) │ ├── orders/ │ │ ├── orders.service.ts ← can call users.service but │ │ └── ... NEVER users.repository directly │ └── payments/ └── app.ts Rules: ✓ Services communicate through public interfaces only ✓ No cross-module database access ✓ Enforced by TypeScript module boundaries or linting rules Benefits: ✓ Simple deployment (one process) ✓ No network latency between modules ✓ Easy debugging (no distributed tracing required) ✓ Can be split into microservices later when load justifies it ✓ ACID transactions still work!
When Microservices Are Actually Justified
✓ Different teams owning different domains independently ✓ Dramatically different scaling requirements per domain (payments needs 99.999% uptime, recommendations can lag) ✓ Different technology requirements (ML service in Python, API in TypeScript) ✓ Regulatory isolation (payment data must not co-reside with user data) ✓ Independent deployment velocity is genuinely needed Warning signs you chose microservices too early: ✗ You spend more time on infrastructure than features ✗ Every feature change requires updating 3+ services ✗ Your team is < 20 engineers ✗ You have no clear domain boundaries ✗ You're doing distributed monolith (all services deploy together)
Mini Project (20-30 min)
Implement a circuit breaker pattern for inter-service communication.
▶ View Solution
class CircuitBreaker {
state = 'CLOSED';
failures = 0;
threshold = 3;
resetTime = 5000;
lastFailure = 0;
async execute(fn: () => Promise) {
if (this.state === 'OPEN') {
if (Date.now() - this.lastFailure > this.resetTime) {
this.state = 'HALF_OPEN';
} else {
throw new Error('Circuit Open');
}
}
try {
const result = await fn();
this.state = 'CLOSED';
this.failures = 0;
return result;
} catch (err) {
this.failures++;
this.lastFailure = Date.now();
if (this.failures >= this.threshold) this.state = 'OPEN';
throw err;
}
}
}
Bigger Project (1-2 hours)
Architect and deploy a set of 3 microservices (e.g., Users, Orders, Inventory) that communicate via an event bus (RabbitMQ or Kafka) using the Saga pattern for distributed transactions.
\n▶ View Solution
// Example gRPC service definition
service Inventory {
rpc CheckStock (StockRequest) returns (StockResponse);
}
Interview Questions
\nCore: What is the Strangler Fig pattern?
It is a pattern for migrating a monolithic application to a microservices architecture. You gradually replace specific pieces of functionality with new microservices. An API gateway intercepts requests and routes them to either the legacy monolith or the new microservices until the monolith can be entirely decommissioned.
Hard: Explain how you would safely migrate a monolithic database to a microservices architecture without downtime.
I would use the Strangler Fig pattern combined with a synchronization strategy. First, duplicate the data to the new microservice DB. Next, modify the monolith to write to both DBs (dual writes) to keep them in sync. Then, switch reads to the new DB. Finally, switch writes entirely to the new microservice and deprecate the monolith's DB tables.
Senior: When would you choose choreography over orchestration for saga-based distributed transactions?
Choreography is better for simple workflows with few services, as it's decentralized and reduces single points of failure. However, as workflows grow complex, choreography leads to cyclic dependencies and makes the overall process hard to monitor. Orchestration (via a central coordinator) is preferred when you need complex compensation logic, centralized visibility, and strict control over the transaction lifecycle.
Revision Sheet
- Boundaries: Domain-driven, not technical-layer-driven
- Data: Each service owns its database. No cross-service DB access.
- Communication: Sync (REST/gRPC) for queries, async (events) for commands
- Transactions: Use Sagas with compensating transactions
- Default: Start with modular monolith. Microservices are justified by org scale, not ambition.