Microservices#
Many small services, each owned by a small team, each independently deployable. Communication happens over the network.
flowchart LR
client([Client]) --> gateway[API Gateway]
subgraph S[Services]
users[Users]
orders[Orders]
billing[Billing]
end
subgraph D[Data]
udb[(Users DB)]
odb[(Orders DB)]
bdb[(Billing DB)]
end
bus[(Event Bus)]
gateway --> users
gateway --> orders
users --> udb
orders --> odb
billing --> bdb
orders --> bus
bus --> billing
Structure#
What a microservices architecture looks like in practice. Each service is independently owned, independently deployed, and talks to the others over the network. Anything shared (databases especially) is the warning sign.
Each service has its own repo (or its own bounded directory in a monorepo).
Each service has its own data store; cross-service reads go through APIs.
Each service is built and deployed independently.
Services communicate via HTTP / gRPC / messaging; never via shared databases.
Strengths#
What microservices buy that a monolith doesn’t. The headline is team autonomy at scale; per-service scaling and stack diversity are the technical wins. None of these matter much below ~20 engineers, which is why microservices are the wrong default for small teams.
Independent scaling, scale only the services that need it.
Stack diversity, pick the right language / framework per service (within limits).
Team autonomy, a team can ship without coordinating with others.
Blast-radius isolation, a failure in one service doesn’t bring down the rest (when designed for it).
Polyglot persistence, relational here, key-value there, search somewhere else.
Costs#
The hidden bill is steep. Most of it lands on operations rather than development; distributed debugging, network failure handling, deployment complexity, and observability infrastructure all become standing requirements rather than nice-to-haves.
Distributed debugging, a request crosses N services; one is slow, one is broken, one is fine.
Network failures everywhere, timeouts, retries, idempotency, circuit breakers must be everywhere.
Data consistency across services, no global transaction; all state is eventually consistent.
Deployment complexity, many pipelines, many artifacts, many versions in production at once.
Observability is a project, distributed tracing, log aggregation, per-service metrics; without them you’re flying blind.
Pre-requisites#
Microservices that work need most of these in place before the split. Without them, the team trades a manageable monolith for an unmanaged distributed system. The list below is the minimum operational baseline.
Mature CI/CD that can independently ship dozens of services.
Centralized logging, metrics, tracing.
A standard service template (logging, health checks, metrics, error handling).
Deployment platform (Kubernetes, Nomad, ECS, serverless).
A discovery mechanism (DNS, service mesh, registry).
On-call ownership matched to service boundaries.
Without these, microservices are an anti-pattern.
Common Pitfalls#
The five ways microservices go wrong in real codebases. Each one results in some of the costs of distribution and few of the benefits; the fix usually involves removing services or removing coupling, not adding more services.
Distributed monolith, services that must deploy together. Worst of both worlds. Fixed by removing the coupling, not adding more services.
Chatty services, every action requires N synchronous calls. Fixed by aggregating data closer to where it’s used or moving to async.
Shared database, accidental coupling through schema. Fixed by per-service schemas.
Synchronous chains, A calls B calls C calls D; D’s slowness becomes A’s slowness. Fixed by async + caching.
Service-per-developer, each new feature gets its own service. Fixed by team-aligned boundaries.
When Microservices Earn Their Keep#
The conditions where microservices actually pay back the operational tax. The defensible default for new products is modular monolith now, extract services as boundaries become real, but the situations below are where the extraction genuinely earns its keep.
Multiple teams that need to ship independently.
Sharply different scaling profiles across components.
Different compliance scopes (PCI vs. non-PCI).
Different language/runtime needs that aren’t a vanity choice.
Sufficient operational maturity to absorb the complexity.
The defensible default for new products is modular monolith now, extract services as boundaries become real.
Service Boundaries#
The most important design decision. Bad boundaries are the single biggest cause of distributed monoliths; get them right and the system absorbs change well; get them wrong and every feature involves coordinating four services. Heuristics.
Bounded contexts (DDD), around stable business domains.
Team ownership, one team should fully own a service.
Rate of change, code that changes together should live together.
Data locality, a service should own its data.
Bad boundaries (by technical layer like UI service, “service” service, DB service, or by entity like UserService, OrderService) create distributed monoliths.
Communication#
The wiring between services. Two axes: synchronous vs asynchronous, and choreography vs orchestration. Most healthy microservices systems use a mix; sync calls where a result is needed, async events for everything else.
Sync (HTTP, gRPC), when the caller needs the result before proceeding.
Async (events on a broker), when the caller can fire-and-forget or react later.
Choreography, services react to events independently; no central coordinator.
Orchestration, a coordinator drives a multi-service workflow (saga).
See Communication.
Data Consistency#
Distributed transactions don’t work in practice; the patterns below are what production microservices systems actually do instead. Sagas for multi-service workflows, the outbox pattern for atomic writes, and eventual consistency surfaced to users through “pending” states.
No distributed transactions in practice.
Sagas for multi-service business processes; design compensations for partial failures.
Outbox pattern, writes to the local DB and an outbox table in one transaction; a separate process publishes from the outbox.
Eventual consistency is the default; the user-visible behavior must reflect it (status pages, “pending” states).
Reliability Patterns#
Every cross-service call needs the four primitives below. Without them, transient downstream issues cascade into system-wide outages; a slow dependency drags every caller into the same slowdown until the whole system stalls.
Timeouts (always).
Retries with exponential backoff and jitter (idempotent calls only).
Circuit breakers to fail fast when downstream is unhealthy.
Bulkheads (per-dependency connection pools).
See Reliability.
Observability#
Required, not optional. In a monolith, grep and a stack
trace are usually enough to diagnose anything; in a microservices
system, debugging without distributed tracing, structured
logging, and per-service metrics is just guessing.
Distributed tracing, W3C trace context everywhere.
Structured logs with correlation IDs.
Per-service metrics, request rate, error rate, latency percentiles.
Service-level objectives (SLOs), agreed-upon reliability targets.
See Observability.