Microservices
A microservices architecture splits an application into small services, each responsible for one business capability (orders, payments, inventory) and each deployable on its own. Services own their data and talk to each other over the network through APIs or messages.
The goal is organizational as much as technical: let many teams change and release their part of the system without coordinating with everyone else.
Diagram
Section titled “Diagram”flowchart LR
client[Web and mobile clients] --> gw[API gateway]
gw --> orders[Orders service]
gw --> catalog[Catalog service]
gw --> payments[Payments service]
orders --> odb[(Orders DB)]
catalog --> cdb[(Catalog DB)]
payments --> pdb[(Payments DB)]
orders -- OrderPlaced event --> bus{{Message broker}}
bus --> payments
bus --> notify[Notification service]
How it works
Section titled “How it works”- One capability per service. Boundaries follow business capabilities or domain-driven design bounded contexts, not technical layers.
- Database per service. No other service reads a service’s tables directly. Data is shared only through its API or events.
- Independent deployment. Each service has its own pipeline and can be released without redeploying the others.
- Communication over the network. Synchronous calls (REST, gRPC) for queries that need an immediate answer, and asynchronous events for everything that can wait.
- Edge routing. An API gateway handles routing, authentication and rate limiting for external clients.
- Independent releases: teams ship on their own schedule.
- Targeted scaling: scale only the services under load.
- Fault isolation: with good design, one failing service degrades a feature instead of the whole application.
- Technology freedom: each service can use the language or datastore that suits it.
- Clear ownership: a team owns a service end to end.
- Distributed-systems complexity: network failures, latency, retries and partial outages become everyday concerns.
- Data consistency is harder: transactions across services need sagas or eventual consistency instead of a single database transaction.
- Operational overhead: dozens of pipelines, deployments, dashboards and on-call rotations.
- Harder debugging: a single request may cross many services, so distributed tracing becomes essential.
- Testing across services: contract tests and integration environments are needed to catch breaking changes.
When to use it
Section titled “When to use it”- Several teams work on the same product and block each other’s releases.
- Parts of the system have very different scaling or availability needs.
- The domain is well understood, so service boundaries can be drawn with confidence.
- The organization already has mature CI/CD, observability and platform automation.
When to avoid it
Section titled “When to avoid it”- A small team or an early-stage product whose domain is still changing. Boundaries drawn too early are expensive to move.
- No automated deployment or monitoring in place yet.
- The main motivation is “it’s modern” rather than a specific scaling or team problem.
In these cases, a modular monolith usually delivers most of the benefit at a fraction of the cost.
Common pitfalls
Section titled “Common pitfalls”- The distributed monolith. Services that must be deployed together, or that call each other in long synchronous chains, carry the costs of both styles and the benefits of neither.
- Shared databases. Two services writing to the same tables are coupled, whatever the deployment diagram says.
- Services that are too small. Splitting by entity (“customer-address-service”) instead of capability multiplies network calls for no gain.
- Ignoring idempotency. Retries and redelivered messages will happen; handlers must tolerate duplicates. See idempotency.
- Observability as an afterthought. Without correlation IDs and tracing from day one, production incidents become guesswork.
Reference implementation
Section titled “Reference implementation”A production-shaped blueprint. Components are described generically first, then with one concrete example stack; the cloud mapping shows equivalents on each major provider.
Scenario
Section titled “Scenario”An online ordering platform: customers browse a catalog, place orders and pay. Three teams own three services: Orders, Payments and Catalog. A Notifications service sends confirmations. Orders must never be charged twice, and a slow catalog must not stop customers from paying.
Component blueprint
Section titled “Component blueprint”flowchart LR
web[Web and mobile apps] --> gw[API gateway]
gw --> orders[Orders service]
gw --> catalog[Catalog service]
orders -- sync: price check --> catalog
orders --> odb[(Orders DB<br/>+ outbox table)]
odb -. change data capture .-> relay[Outbox relay]
relay --> broker{{Event broker}}
broker --> payments[Payments service]
payments --> pdb[(Payments DB<br/>+ processed events)]
payments --> psp[Payment provider]
payments -- PaymentCaptured --> broker
broker --> orders
broker --> notify[Notifications service]
subgraph platform [Platform]
obs[Tracing, metrics, logs]
sec[Secrets manager]
end
| Component | Responsibility | Vendor-neutral choice | Example stack |
|---|---|---|---|
| API gateway | Routing, authentication, rate limiting | Any API gateway | Kong (opens in a new tab) |
| Services | One business capability each | Containerized services | Java 21, Spring Boot (opens in a new tab) 3 |
| Service databases | Each service’s private data | Relational database per service | PostgreSQL (opens in a new tab) |
| Outbox relay | Publish events written to the outbox table | Change data capture | Debezium (opens in a new tab) |
| Event broker | Asynchronous events between services | Durable log or message broker | Apache Kafka (opens in a new tab) |
| Runtime | Deploy, scale and heal services | Container orchestrator | Kubernetes (opens in a new tab) |
| Observability | Traces, metrics, logs across services | OpenTelemetry (opens in a new tab)-based stack | OpenTelemetry, Prometheus (opens in a new tab), Grafana (opens in a new tab) |
| Secrets | Credentials and keys | Secrets manager | HashiCorp Vault (opens in a new tab) |
Request flow: placing an order
Section titled “Request flow: placing an order”sequenceDiagram
participant C as Customer
participant G as API gateway
participant O as Orders
participant K as Kafka
participant P as Payments
participant X as Payment provider
C->>G: POST /orders (Idempotency-Key)
G->>O: forward with user identity
O->>O: save order + OrderPlaced in outbox (one transaction)
O-->>C: 202 Accepted, order PENDING
O--)K: relay publishes OrderPlaced
K--)P: OrderPlaced
P->>P: skip if event already processed
P->>X: capture payment (idempotency key = order id)
P--)K: PaymentCaptured
K--)O: PaymentCaptured
O->>O: order status = CONFIRMED
The customer gets an immediate response; payment happens asynchronously, and every step can be safely retried.
Key implementation details
Section titled “Key implementation details”1. Write the order and its event atomically (transactional outbox). The event is stored in the same database transaction as the order, so one can never exist without the other.
CREATE TABLE outbox ( id UUID PRIMARY KEY, aggregate_type TEXT NOT NULL, -- e.g. 'Order' aggregate_id TEXT NOT NULL, -- used as the message key event_type TEXT NOT NULL, -- e.g. 'OrderPlaced' payload JSONB NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT now());@Servicepublic class OrderService {
private final OrderRepository orders; private final OutboxRepository outbox;
@Transactional public Order placeOrder(PlaceOrderCommand cmd) { Order order = orders.save(Order.pending(cmd)); outbox.save(OutboxEvent.of( "Order", order.getId().toString(), "OrderPlaced", OrderPlaced.from(order))); return order; // both rows commit together, or neither does }}A change-data-capture tool such as Debezium reads new outbox rows from the database log and publishes them to Kafka, keyed by aggregate_id so events for one order stay in order.
2. Make every consumer idempotent. Kafka delivers at least once, so the Payments service records each processed event ID in the same transaction as its own changes.
@KafkaListener(topics = "orders.order-placed", groupId = "payments")@Transactionalpublic void onOrderPlaced(OrderPlaced event) { int firstTime = jdbc.update( "INSERT INTO processed_events (event_id) VALUES (?) ON CONFLICT DO NOTHING", event.eventId()); if (firstTime == 0) { return; // duplicate delivery: already handled } payments.capture(event.orderId(), event.amount());}3. Contain slow dependencies. Calls between services get a timeout, limited retries and a circuit breaker, here with Resilience4j (opens in a new tab):
@CircuitBreaker(name = "catalog", fallbackMethod = "lastKnownPrice")@Retry(name = "catalog")public Price currentPrice(String sku) { return catalogClient.price(sku); // HTTP client with a 500 ms timeout}
private Price lastKnownPrice(String sku, Throwable cause) { return priceCache.get(sku); // degrade instead of failing the order}4. Run each service with health checks, limits and autoscaling.
apiVersion: apps/v1kind: Deploymentmetadata: name: ordersspec: replicas: 3 selector: matchLabels: { app: orders } template: metadata: labels: { app: orders } spec: containers: - name: orders image: registry.example.com/orders:1.4.2 ports: [{ containerPort: 8080 }] env: - name: JAVA_TOOL_OPTIONS value: "-javaagent:/otel/opentelemetry-javaagent.jar" - name: OTEL_SERVICE_NAME value: orders resources: requests: { cpu: "250m", memory: "512Mi" } limits: { memory: "1Gi" } readinessProbe: httpGet: { path: /actuator/health/readiness, port: 8080 } livenessProbe: httpGet: { path: /actuator/health/liveness, port: 8080 }The OpenTelemetry Java agent adds distributed tracing without code changes, so one trace follows an order from the gateway through Kafka to Payments.
Cloud service mapping
Section titled “Cloud service mapping”Service names change over time; check each provider’s current catalog.
Run it yourself
Section titled “Run it yourself”The microservices reference implementation (opens in a new tab) builds the Orders and Payments services six times against one shared contract (opens in a new tab). It runs on a laptop with Docker Compose (opens in a new tab) and keeps the infrastructure minimal: a polling outbox relay instead of Debezium, and no gateway, Kubernetes or tracing.
git clone https://github.com/architecture-compass/reference-implementationscd reference-implementations/microservicesdocker compose --profile go up -d --build # or: java, python, rust, dotnet, node./demo.shThe demo places an order, repeats it with the same idempotency key, follows it through the outbox, Kafka and Payments to CONFIRMED, redelivers a duplicate event, and stops the catalog to show the circuit breaker and price fallback. Every language passes all seven checks.
Want to go further? The README’s Break it yourself (opens in a new tab) section has hands-on experiments that show each pattern at work, including what happens when things fail.
Production readiness checklist
Section titled “Production readiness checklist”- Each service owns its database; no shared tables
- Events published through an outbox, never directly after a commit
- Every consumer is idempotent and has a dead-letter topic
- Timeouts, retries and circuit breakers on every synchronous call
- Traces propagate across HTTP calls and Kafka messages
- Each service deploys independently through its own pipeline
- Contract tests catch breaking API and event changes before release
- Runbooks and alerts per service, owned by the team that runs it
In practice
Section titled “In practice”- Netflix moves to the cloud: rebuilding a monolith as hundreds of services.
- Airbnb splits its monolith: validating each new service against the monolith on live traffic.
- Monzo runs a bank on microservices: keeping thousands of services manageable with a paved road.
- Uber organizes services into domains: grouping 2,200 services behind domain gateways.
- Segment goes back to a monolith: when per-unit services cost more than they save.
Related patterns
Section titled “Related patterns”- Event-driven architecture
- API gateway
- Saga
- Strangler fig migration
- Circuit breaker
- Backend for frontend
Glossary: bounded context, eventual consistency, saga, idempotency
Further reading
Section titled “Further reading”- Sam Newman, Building Microservices (opens in a new tab) (2nd edition)
- Chris Richardson, Microservices Patterns (opens in a new tab)
- James Lewis and Martin Fowler, “Microservices” (opens in a new tab)
Written and edited by Abhinav Taduka, enterprise software architect.