Module 9: Why Microservices Exist
Microservices did not emerge because someone thought "let's make everything harder." They emerged because organizations at scale — Amazon, Netflix, Spotify — hit concrete walls with monolithic architectures. Walls that couldn't be solved by better code, cleaner modules, or faster hardware. The walls were organizational and operational.
Before diving into how to decompose services, you need to understand the forces that make microservices the correct architectural response — and the forces that make them catastrophically wrong.
Conway's Law Applied: Teams Shape Architecture
We covered Conway's Law in Part 4, but here it becomes operational. The law states: "Any organization that designs a system will produce a design whose structure is a copy of the organization's communication structure."
The inverse Conway maneuver is the deliberate practice: structure your teams around the services you want to exist, and the architecture follows. Amazon's famous mandate (2002) didn't start with "let's build microservices." It started with: "every team will expose their functionality through service interfaces." The architecture was a consequence of the organizational design.
flowchart LR
subgraph ORG["Organization Structure"]
T1["Order Team
8 engineers"]
T2["Payment Team
6 engineers"]
T3["Inventory Team
5 engineers"]
T4["Shipping Team
7 engineers"]
end
subgraph ARCH["System Architecture"]
S1["Order Service"]
S2["Payment Service"]
S3["Inventory Service"]
S4["Shipping Service"]
end
T1 --> S1
T2 --> S2
T3 --> S3
T4 --> S4
S1 -->|"API"| S2
S1 -->|"API"| S3
S1 -->|"Event"| S4
style T1 fill:#e8f4f4,stroke:#3B9797,color:#132440
style T2 fill:#e8f4f4,stroke:#3B9797,color:#132440
style T3 fill:#e8f4f4,stroke:#3B9797,color:#132440
style T4 fill:#e8f4f4,stroke:#3B9797,color:#132440
style S1 fill:#f0f4f8,stroke:#16476A,color:#132440
style S2 fill:#f0f4f8,stroke:#16476A,color:#132440
style S3 fill:#f0f4f8,stroke:#16476A,color:#132440
style S4 fill:#f0f4f8,stroke:#16476A,color:#132440
When one team owns one service, they can:
- Deploy independently — no coordinating with 14 other teams for a release train
- Choose their own technology — the Payment team can use Go for performance while the Order team uses Python for rapid iteration
- Scale independently — the Inventory service handles 10× more reads than the Payment service; scale them differently
- Fail independently — if the Shipping service crashes, customers can still browse and order
Independent Deployment: The Primary Driver
The single most important benefit of microservices is independent deployability. If you can't deploy services independently, you don't have microservices — you have a distributed monolith (more on that anti-pattern later).
Independent deployment means:
- Changing Service A requires deploying only Service A
- No lockstep releases across services
- No shared deployment pipelines that block on other teams
- Rollback of Service A doesn't require rolling back Service B
This property directly enables team autonomy. A team that owns a service can release 50 times per day or once per sprint — their cadence doesn't constrain or get constrained by other teams.
Technology Freedom (With Guardrails)
Since each service communicates only through well-defined APIs, the implementation behind that API is irrelevant to consumers. This enables polyglot architectures — different services using different languages, frameworks, and datastores. The Payment service might use Rust for memory safety in financial calculations. The Recommendation service might use Python for ML model serving. The Order service might use Java for its mature ecosystem of enterprise patterns.
However, technology freedom has a cost: operational diversity. Every new language means new deployment pipelines, monitoring integrations, debugging tools, and hiring requirements. Smart organizations constrain this freedom with a "golden path" — 2-3 supported stacks with full platform support, and an escape hatch for exceptions that carry their own operational burden.
Decomposition Strategies
The hardest question in microservices isn't "how do I build a service?" — it's "where do I draw the boundaries?" Wrong boundaries create services that must always change together, communicate excessively, and share data underneath. There are two primary decomposition strategies:
Strategy 1: Decompose by Business Capability
A business capability is something the business does to generate value. It's stable over time — even as the implementation changes. Examples:
- Order Management — accepting, tracking, and fulfilling customer orders
- Payment Processing — charging customers, handling refunds, managing payment methods
- Inventory Management — tracking stock levels, reserving inventory, triggering replenishment
- Customer Communication — sending emails, push notifications, SMS
Each capability becomes a service. The key insight: business capabilities rarely change (you'll always need to "process payments"), even though the implementation evolves constantly. This gives service boundaries longevity.
Strategy 2: Decompose by Subdomain (DDD)
Domain-Driven Design classifies subdomains into three types, each warranting different investment levels:
| Subdomain Type | Definition | Example | Build vs. Buy |
|---|---|---|---|
| Core | What differentiates you from competitors. Your competitive advantage. | Uber's ride matching algorithm, Netflix's recommendation engine | Build custom. Invest heavily. |
| Supporting | Necessary for the core to function, but not differentiating. | Driver onboarding, content ingestion pipeline | Build, but don't over-engineer. |
| Generic | Same across all businesses. No competitive value. | Authentication, email sending, payment gateway integration | Buy/use SaaS. Don't build. |
flowchart TD
START["Identify a business function"] --> Q1{"Is it your competitive
advantage?"}
Q1 -->|Yes| CORE["Core Subdomain
Build custom service
Best engineers here"]
Q1 -->|No| Q2{"Does it support
core functions?"}
Q2 -->|Yes| SUPPORTING["Supporting Subdomain
Build pragmatically
Keep it simple"]
Q2 -->|No| GENERIC["Generic Subdomain
Buy/SaaS
Don't build this"]
CORE --> Q3{"Does the team own
the full lifecycle?"}
SUPPORTING --> Q3
Q3 -->|Yes| SERVICE["✅ Good service
boundary"]
Q3 -->|No| RETHINK["⚠️ Rethink boundary
Shared ownership = coupling"]
style CORE fill:#e8f4f4,stroke:#3B9797,color:#132440
style SUPPORTING fill:#f0f4f8,stroke:#16476A,color:#132440
style GENERIC fill:#fff5f5,stroke:#BF092F,color:#132440
style SERVICE fill:#e8f4f4,stroke:#3B9797,color:#132440
style RETHINK fill:#fff5f5,stroke:#BF092F,color:#132440
Bounded Contexts: The DDD Foundation
A bounded context is the most critical concept from Domain-Driven Design for microservices architecture. It defines a boundary within which a particular domain model is consistent and meaningful. Outside that boundary, the same word can mean something entirely different.
Key bounded context principles:
- Ubiquitous Language — within a bounded context, every term has exactly one meaning, shared between developers and domain experts. "Order" in the Fulfillment context means a shipping instruction. "Order" in the Sales context means a revenue event.
- Context Maps — explicit documentation of how bounded contexts relate to each other: who is upstream, who is downstream, what translation happens at the boundary.
- Anti-Corruption Layers (ACL) — translation layers at context boundaries that prevent one context's model from leaking into another. The Shipping service doesn't import the Sales service's Customer class — it maintains its own Recipient model and translates at the boundary.
flowchart TB
subgraph SALES["Sales Context"]
SC["Customer = prospect
Deal, Pipeline, Revenue"]
end
subgraph ORDERS["Order Context"]
OC["Customer = buyer
Cart, Order, Payment"]
end
subgraph SHIPPING["Shipping Context"]
SHC["Customer = recipient
Address, Delivery, Tracking"]
end
subgraph BILLING["Billing Context"]
BC["Customer = account
Invoice, Payment Method, Credit"]
end
SALES -->|"ACL: translate
prospect → buyer"| ORDERS
ORDERS -->|"Event: OrderPlaced
ACL: buyer → recipient"| SHIPPING
ORDERS -->|"Event: OrderConfirmed
ACL: buyer → account"| BILLING
style SC fill:#e8f4f4,stroke:#3B9797,color:#132440
style OC fill:#f0f4f8,stroke:#16476A,color:#132440
style SHC fill:#f5f0f8,stroke:#6B4C9A,color:#132440
style BC fill:#fdf5e6,stroke:#D4880F,color:#132440
API Contracts and Compatibility
In a monolith, interfaces between modules are checked at compile time. In microservices, API contracts are the only thing holding the system together. Breaking a contract breaks consumers — potentially at 3 AM on a Saturday when the team that owns the consumer is on vacation.
Consumer-Driven Contracts
Traditional API design is provider-centric: "here's what I expose, deal with it." Consumer-driven contracts flip this: each consumer declares what it needs from the provider, and the provider's CI pipeline verifies that ALL consumer contracts still pass before deploying.
{
"consumer": "order-service",
"provider": "inventory-service",
"interactions": [
{
"description": "Check stock availability for a product",
"request": {
"method": "GET",
"path": "/api/v1/inventory/products/PROD-12345/availability",
"headers": {
"Accept": "application/json",
"X-Correlation-ID": "uuid-format"
}
},
"response": {
"status": 200,
"headers": {
"Content-Type": "application/json"
},
"body": {
"productId": "PROD-12345",
"available": true,
"quantity": 42,
"warehouse": "us-east-1"
}
}
},
{
"description": "Reserve inventory for an order",
"request": {
"method": "POST",
"path": "/api/v1/inventory/reservations",
"headers": {
"Content-Type": "application/json"
},
"body": {
"productId": "PROD-12345",
"quantity": 2,
"orderId": "ORD-99999",
"expiresIn": "15m"
}
},
"response": {
"status": 201,
"body": {
"reservationId": "RES-00001",
"status": "confirmed",
"expiresAt": "2026-05-15T10:15:00Z"
}
}
}
]
}
Backward Compatibility Rules
API evolution must follow strict compatibility rules to avoid breaking consumers:
- Adding fields — always safe. Consumers ignore unknown fields (Postel's Law).
- Removing fields — breaking change. Deprecate first, remove after all consumers migrate.
- Changing field types — breaking change. Add new field with new type, deprecate old.
- Adding required parameters — breaking change. Make them optional with defaults.
- Semantic versioning — MAJOR (breaking), MINOR (additive), PATCH (fixes). URL-versioned APIs:
/api/v1/,/api/v2/.
Communication Patterns: Sync vs. Async
How services talk to each other is the most impactful architectural decision in a microservices system. The two fundamental patterns are synchronous (request/response) and asynchronous (event-driven).
| Pattern | Mechanism | When to Use | Tradeoff |
|---|---|---|---|
| Synchronous — REST | HTTP request/response. JSON payloads. Stateless. | Simple CRUD, external APIs, human-facing latency requirements | Temporal coupling: caller blocks until response. Cascading failures. |
| Synchronous — gRPC | HTTP/2, Protocol Buffers. Schema-first. Streaming support. | Internal service-to-service. High throughput. Strong typing needed. | Schema evolution complexity. Harder to debug than JSON. |
| Asynchronous — Events | Publish/subscribe. Kafka, RabbitMQ, SNS/SQS. | Notifications, eventual consistency, decoupled workflows. | Debugging difficulty. Ordering guarantees. Duplicate handling. |
| Asynchronous — Commands | Point-to-point messaging. One producer, one consumer. | Task delegation, work queues, reliable execution. | Dead letter queues needed. Retry complexity. |
flowchart LR
subgraph SYNC["Synchronous (Request/Response)"]
A1["Order Service"] -->|"REST/gRPC
Blocks until response"| B1["Payment Service"]
A1 -->|"REST
Inventory check"| C1["Inventory Service"]
end
subgraph ASYNC["Asynchronous (Event-Driven)"]
A2["Order Service"] -->|"Publish:
OrderPlaced"| MQ["Message Broker
(Kafka/RabbitMQ)"]
MQ -->|"Subscribe"| B2["Email Service"]
MQ -->|"Subscribe"| C2["Analytics Service"]
MQ -->|"Subscribe"| D2["Shipping Service"]
end
style A1 fill:#e8f4f4,stroke:#3B9797,color:#132440
style B1 fill:#f0f4f8,stroke:#16476A,color:#132440
style C1 fill:#f0f4f8,stroke:#16476A,color:#132440
style A2 fill:#e8f4f4,stroke:#3B9797,color:#132440
style MQ fill:#fdf5e6,stroke:#D4880F,color:#132440
style B2 fill:#f0f4f8,stroke:#16476A,color:#132440
style C2 fill:#f0f4f8,stroke:#16476A,color:#132440
style D2 fill:#f0f4f8,stroke:#16476A,color:#132440
Data Ownership: Each Service Owns Its Data
The most violated principle in microservices: each service owns its data exclusively. No shared databases. No direct SQL queries from Service A into Service B's tables. If Service A needs Service B's data, it calls Service B's API or subscribes to Service B's events.
Why this rule is non-negotiable:
- Coupling through data — If services share a database, they cannot evolve their schemas independently. Changing a column in the shared "customers" table requires coordinating with every service that reads it.
- Deployment coupling — Schema migrations become distributed coordination problems. You can't deploy Service A's migration without verifying it won't break Service B.
- Scaling independence destroyed — A shared database is a shared bottleneck. One service's query load affects all others.
# kubernetes service definition - order-service with its own database
apiVersion: apps/v1
kind: Deployment
metadata:
name: order-service
labels:
app: order-service
team: order-team
domain: commerce
spec:
replicas: 3
selector:
matchLabels:
app: order-service
template:
metadata:
labels:
app: order-service
spec:
containers:
- name: order-service
image: registry.example.com/order-service:v2.4.1
ports:
- containerPort: 8080
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: order-db-credentials
key: connection-string
- name: KAFKA_BROKERS
value: "kafka-cluster:9092"
- name: SERVICE_NAME
value: "order-service"
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
livenessProbe:
httpGet:
path: /health/live
port: 8080
initialDelaySeconds: 10
periodSeconds: 15
readinessProbe:
httpGet:
path: /health/ready
port: 8080
initialDelaySeconds: 5
periodSeconds: 10
---
apiVersion: v1
kind: Service
metadata:
name: order-service
spec:
selector:
app: order-service
ports:
- protocol: TCP
port: 80
targetPort: 8080
type: ClusterIP
Challenges Deep-Dive
Now for the uncomfortable truth. Every benefit of microservices comes with a corresponding cost. The industry has spent a decade celebrating the benefits while burying the costs in "operational maturity" hand-waving. Let's be explicit about what you're buying when you choose microservices.
Distributed Complexity
In a monolith, a function call takes nanoseconds, always succeeds (no network), and returns a result or throws an exception. In microservices, the same logical operation becomes:
- Serialize the request (CPU cost + allocation)
- Transmit over the network (latency + possible failure)
- Deserialize at the receiver (CPU cost)
- Process the request
- Serialize the response
- Transmit back over the network (latency + possible failure)
- Deserialize the response
- Handle: timeout? Connection refused? 500 error? Partial failure? Retry?
Every network call introduces 8 failure modes that don't exist in a monolith. Multiply by the number of inter-service calls in a request path, and you understand why distributed systems are fundamentally harder.
#!/bin/bash
# health-check-cascade.sh
# Check health of all services in a request chain
# Demonstrates the operational complexity of microservices
echo "=== Microservice Health Check ==="
echo "Checking entire order-placement chain..."
echo ""
SERVICES=(
"order-service:8080"
"inventory-service:8081"
"payment-service:8082"
"notification-service:8083"
"shipping-service:8084"
)
HEALTHY=0
UNHEALTHY=0
for SERVICE in "${SERVICES[@]}"; do
NAME=$(echo "$SERVICE" | cut -d: -f1)
PORT=$(echo "$SERVICE" | cut -d: -f2)
RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" \
--connect-timeout 2 --max-time 5 \
"http://${SERVICE}/health/ready")
if [ "$RESPONSE" == "200" ]; then
echo " ✅ ${NAME} — healthy (${RESPONSE})"
((HEALTHY++))
else
echo " ❌ ${NAME} — UNHEALTHY (HTTP ${RESPONSE:-timeout})"
((UNHEALTHY++))
fi
done
echo ""
echo "Results: ${HEALTHY} healthy, ${UNHEALTHY} unhealthy"
echo ""
if [ $UNHEALTHY -gt 0 ]; then
echo "⚠️ WARNING: Order placement chain is degraded!"
echo " Affected capability: Customer cannot complete checkout"
echo " Blast radius: All new orders blocked"
exit 1
else
echo "✅ All services healthy — order placement chain operational"
fi
Operational Overhead
A monolith needs: one deployment pipeline, one log stream, one monitoring dashboard, one database to back up, one server to provision. A microservices architecture with 20 services needs: 20 deployment pipelines, distributed log aggregation (ELK/Datadog), distributed tracing (Jaeger/Zipkin), 20 databases, a container orchestrator (Kubernetes), a service mesh (Istio/Linkerd), secrets management, and a team to operate all of this.
The minimum viable platform for microservices includes:
- Container orchestration — Kubernetes or equivalent (ECS, Nomad)
- Service discovery — DNS-based or service registry
- Distributed tracing — correlate requests across 5+ service hops
- Centralized logging — aggregate logs from 20+ services into one query interface
- CI/CD per service — independent build/test/deploy pipelines
- Circuit breakers — prevent cascade failures when a downstream service dies
- Secrets management — rotate credentials across 20 services without downtime
If your organization doesn't have a platform team or the engineering maturity to build/maintain this infrastructure, microservices will slow you down, not speed you up.
Data Consistency: The Hardest Problem
In a monolith, you wrap multiple operations in a database transaction: either all succeed or all rollback. In microservices with separate databases, there is no distributed ACID transaction that works reliably at scale. You're left with two options:
- Saga Pattern — a sequence of local transactions with compensating transactions for rollback. If Step 3 of 5 fails, you execute compensating actions for Steps 1 and 2.
- Eventual Consistency — accept that different services will have temporarily inconsistent views of the world, and design your UI/UX to handle this gracefully.
Both are significantly harder than BEGIN TRANSACTION ... COMMIT. Every distributed workflow requires answering: "What happens if step N fails after steps 1 through N-1 succeeded? How do I undo partial work? What if the undo itself fails?"
The Distributed Monolith Anti-Pattern
flowchart TD
subgraph BAD["❌ Distributed Monolith"]
A["Service A"] -->|sync| B["Service B"]
B -->|sync| C["Service C"]
C -->|sync| D["Service D"]
A -.->|shared DB| DB[(Shared Database)]
B -.->|shared DB| DB
C -.->|shared DB| DB
D -.->|shared DB| DB
end
subgraph GOOD["✅ Proper Microservices"]
E["Service E"] -->|async event| MQ["Event Bus"]
MQ --> F["Service F"]
MQ --> G["Service G"]
E --- DB1[(DB-E)]
F --- DB2[(DB-F)]
G --- DB3[(DB-G)]
end
style A fill:#fff5f5,stroke:#BF092F,color:#132440
style B fill:#fff5f5,stroke:#BF092F,color:#132440
style C fill:#fff5f5,stroke:#BF092F,color:#132440
style D fill:#fff5f5,stroke:#BF092F,color:#132440
style DB fill:#fff5f5,stroke:#BF092F,color:#132440
style E fill:#e8f4f4,stroke:#3B9797,color:#132440
style F fill:#e8f4f4,stroke:#3B9797,color:#132440
style G fill:#e8f4f4,stroke:#3B9797,color:#132440
style MQ fill:#fdf5e6,stroke:#D4880F,color:#132440
style DB1 fill:#e8f4f4,stroke:#3B9797,color:#132440
style DB2 fill:#e8f4f4,stroke:#3B9797,color:#132440
style DB3 fill:#e8f4f4,stroke:#3B9797,color:#132440
Signs you're building a distributed monolith:
- Lockstep deployments — "We need to deploy services A, B, and C together in this order"
- Shared data layer — Services read/write the same database tables
- Deep call chains — A synchronous call chain of 4+ services for a single user action
- Shared libraries with domain logic — A "common" library that contains business rules
- Integration tests that spin up 8 services — If you can't test a service in isolation, it's not independent
// contract-test-order-service.js
// Consumer-driven contract test — verifies the inventory service
// meets the order-service's expectations WITHOUT calling the real service
const { Pact } = require('@pact-foundation/pact');
const { expect } = require('chai');
const axios = require('axios');
const provider = new Pact({
consumer: 'OrderService',
provider: 'InventoryService',
port: 4000,
log: './logs/pact.log',
dir: './pacts',
});
describe('Order Service — Inventory Contract', () => {
before(() => provider.setup());
after(() => provider.finalize());
describe('Check product availability', () => {
before(() => {
return provider.addInteraction({
state: 'product PROD-12345 exists with 42 units',
uponReceiving: 'a request for product availability',
withRequest: {
method: 'GET',
path: '/api/v1/inventory/products/PROD-12345/availability',
headers: { Accept: 'application/json' },
},
willRespondWith: {
status: 200,
headers: { 'Content-Type': 'application/json' },
body: {
productId: 'PROD-12345',
available: true,
quantity: 42,
},
},
});
});
it('returns availability status', async () => {
const response = await axios.get(
'http://localhost:4000/api/v1/inventory/products/PROD-12345/availability',
{ headers: { Accept: 'application/json' } }
);
expect(response.status).to.equal(200);
expect(response.data.productId).to.equal('PROD-12345');
expect(response.data.available).to.be.true;
expect(response.data.quantity).to.be.a('number');
});
afterEach(() => provider.verify());
});
describe('Reserve inventory for order', () => {
before(() => {
return provider.addInteraction({
state: 'product PROD-12345 has sufficient stock',
uponReceiving: 'a request to reserve inventory',
withRequest: {
method: 'POST',
path: '/api/v1/inventory/reservations',
headers: { 'Content-Type': 'application/json' },
body: {
productId: 'PROD-12345',
quantity: 2,
orderId: 'ORD-99999',
expiresIn: '15m',
},
},
willRespondWith: {
status: 201,
body: {
reservationId: 'RES-00001',
status: 'confirmed',
},
},
});
});
it('confirms reservation', async () => {
const response = await axios.post(
'http://localhost:4000/api/v1/inventory/reservations',
{
productId: 'PROD-12345',
quantity: 2,
orderId: 'ORD-99999',
expiresIn: '15m',
},
{ headers: { 'Content-Type': 'application/json' } }
);
expect(response.status).to.equal(201);
expect(response.data.status).to.equal('confirmed');
expect(response.data.reservationId).to.be.a('string');
});
afterEach(() => provider.verify());
});
});
Case Studies
Amazon's Two-Pizza Teams and Service Ownership
In 2002, Jeff Bezos issued the now-famous "API mandate": all teams must expose their functionality through service interfaces, no direct access to another team's data store, and all communication happens through these interfaces — no exceptions. The mandate ended with: "Anyone who doesn't do this will be fired."
The organizational design:
- Two-pizza teams — no team larger than can be fed by two pizzas (~6-8 people)
- "You build it, you run it" — the team that writes the code operates it in production, including on-call
- Full ownership — each team owns their service's entire lifecycle: design, build, deploy, operate, iterate
- Customer-facing metric — every team has a direct connection to a business metric they're accountable for
The result: Amazon now operates thousands of microservices. Each team deploys independently (some deploy 50+ times per day). Team autonomy enables innovation velocity that wouldn't be possible in a coordinated monolith with 10,000+ engineers.
The hidden cost: Amazon invested billions in internal platform tooling (deployment systems, monitoring, service frameworks) that most organizations cannot replicate. The "two-pizza team" model only works with a mature internal developer platform that handles the operational complexity.
Uber's Microservice Explosion and Domain Consolidation
By 2018, Uber had grown from a monolithic Python application to approximately 4,000 microservices. The initial decomposition was driven by rapid team growth (doubling every 6 months) and the need for deployment independence. But by 2018, they faced severe problems:
What went wrong:
- Dependency explosion — a single request to book a ride traversed 70+ services. Understanding the full call graph was impossible.
- Cascading failures — one degraded service caused timeouts in 50+ upstream services. Blast radius was enormous.
- Inconsistent boundaries — services were split by technical layer ("auth-service", "database-proxy") instead of business domain, creating artificial coupling.
- Operational nightmare — 4,000 services meant 4,000 deployment pipelines, monitoring dashboards, and on-call rotations.
The correction — DOMA (Domain-Oriented Microservice Architecture):
- Services grouped into domains (collections of 5-15 related services with a single gateway)
- Cross-domain communication goes through domain gateways — not arbitrary service-to-service calls
- Each domain has a domain owner responsible for the gateway contract and internal service coordination
- Reduced cognitive load: teams think in terms of domains (Rides, Eats, Payments) not individual services
Lesson: Microservices without disciplined domain boundaries leads to a "microservice explosion" that's harder to manage than the monolith it replaced. Consolidation around bounded contexts (DOMA) restored many benefits of both approaches.
Conclusion & Next Steps
Module 9 covered the complete landscape of microservices architecture — from the organizational forces that justify decomposition to the technical patterns that make it work, to the very real challenges that make it expensive.
The key takeaways:
- Microservices are an organizational scaling solution first, a technical pattern second. If your team is small enough to coordinate easily, you probably don't need them.
- Bounded contexts are the correct unit of decomposition. Not technical layers, not arbitrary "this class has too many lines" splits, but domain boundaries where the ubiquitous language changes.
- Independent deployability is the litmus test. If you can't deploy and roll back a service without coordinating with other teams, you have a distributed monolith.
- Each service owns its data. The moment two services share a database, you've coupled them at the most fundamental level.
- The operational cost is enormous. Microservices require a platform (Kubernetes, CI/CD per service, distributed tracing, centralized logging) that costs millions in engineering time to build and maintain.
The honest decision framework: choose microservices when your organizational scaling constraints force you to — when teams can't ship independently, when parts of the system need radically different scaling profiles, when fault isolation is business-critical. Don't choose them because "Netflix does it" or because a conference speaker made monoliths sound embarrassing.
Next in the Series
In Part 7: Event-Driven Architecture & Data Patterns, we'll explore the communication patterns that make microservices actually work — event sourcing, CQRS, saga orchestration, and the art of designing systems that communicate through events rather than synchronous calls.