Microservices
Microservices architecture structures an application as a collection of loosely coupled, independently deployable services. Each service owns a bounded context, has its own data store, and communicates with other services over a network. The goal is to enable team autonomy, independent scaling, and technology diversity.
Microservices are not a free lunch. They trade development simplicity for operational complexity. A system that fits comfortably in a well-modularized monolith is often faster to build, test, and operate than an equivalent microservices system run by a small team.
This guide covers how to identify service boundaries, how services communicate, how they find each other, and the tradeoffs that determine whether microservices are the right choice for your organization.
A monolith is a single deployable unit that contains all functionality. Calls between modules are in-process function calls. A microservices architecture splits functionality into separate processes that communicate over the network.
| Dimension | Monolith | Microservices |
|---|---|---|
| Deployment | Single artifact | Many independent artifacts |
| Communication | In-process function calls | Network calls (HTTP, gRPC, messaging) |
| Data ownership | Shared database | Database per service |
| Team autonomy | Coordination required | Independent release cadence |
| Operational complexity | Low | High — observability, deployments, failures |
| Scalability | Scale the whole unit | Scale individual services |
best practice
The hardest part of microservices is drawing the boundaries. A boundary should align with a business capability, not a technical layer. Services organized by function — orders, payments, inventory — are more stable than services organized by layer — frontend, backend, database.
Bounded Contexts
From Domain-Driven Design, a bounded context is a linguistic boundary within which a domain model is consistent. The word "customer" might mean something different in billing than in support. Each bounded context is a candidate for a microservice.
| 1 | E-commerce bounded contexts: |
| 2 | |
| 3 | ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ |
| 4 | │ Catalog │ │ Cart │ │ Orders │ |
| 5 | │ (products) │ │ (session) │ │ (checkout) │ |
| 6 | └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ |
| 7 | │ │ │ |
| 8 | └───────────────┼───────────────┘ |
| 9 | │ |
| 10 | ┌─────────▼─────────┐ |
| 11 | │ Payments │ |
| 12 | │ (charge/refund) │ |
| 13 | └───────────────────┘ |
| 14 | |
| 15 | Each context owns its own data and exposes |
| 16 | a stable API to other contexts. |
Signs of a Bad Boundary
warning
Services communicate either synchronously or asynchronously. Synchronous calls are simpler to reason about but create temporal coupling: if the downstream service is slow or unavailable, the caller blocks or fails. Asynchronous communication decouples services through message queues or event streams.
Synchronous
HTTP/REST and gRPC are the dominant synchronous protocols. REST is human-readable and widely supported. gRPC is faster and strongly typed but requires HTTP/2 and protobuf tooling. Reserve synchronous calls for operations where the response is needed immediately and the dependency is reliable.
| 1 | Synchronous request chain: |
| 2 | |
| 3 | Client ──▶ API Gateway ──▶ Order Service ──▶ Payment Service |
| 4 | │ |
| 5 | ▼ |
| 6 | Inventory Service |
| 7 | |
| 8 | Risk: any failure in the chain fails the request. |
| 9 | Mitigation: timeouts, retries, circuit breakers."} |
| 10 | filename="sync-communication.txt" |
| 11 | /> |
| 12 | |
| 13 | <h3 className="text-sm font-heading font-medium text-[#E0E0E0] mt-6 mb-3">Asynchronous</h3> |
| 14 | <p className="text-xs font-mono text-[#A0A0A0] leading-relaxed mb-4"> |
| 15 | Asynchronous communication uses message queues or event buses. A service publishes an event and continues; other services consume the event when ready. This improves resilience and throughput but introduces eventual consistency and the need for idempotent consumers. |
| 16 | </p> |
| 17 | |
| 18 | <CodeBlock |
| 19 | language="text" |
| 20 | code={`Asynchronous event flow: |
| 21 | |
| 22 | Order Service publishes OrderPlaced event |
| 23 | │ |
| 24 | ▼ |
| 25 | ┌─────────────┐ |
| 26 | │ Message │ |
| 27 | │ Broker │ |
| 28 | └──────┬──────┘ |
| 29 | │ |
| 30 | ┌────────┼────────┐ |
| 31 | ▼ ▼ ▼ |
| 32 | Payment Inventory Notification |
| 33 | Service Service Service |
| Style | Coupling | Latency | Resilience | Best For |
|---|---|---|---|---|
| Sync REST | Tight | Low | Requires retries/breakers | User-facing queries |
| Sync gRPC | Tight | Very low | Requires retries/breakers | Internal service calls |
| Async queue | Loose | Higher | High — messages persist | Background work, buffering |
| Event stream | Loose | Near-real-time | High — replayable | Event sourcing, analytics |
best practice
In a dynamic environment, service instances come and go. Service discovery lets clients locate healthy instances without hard-coding addresses. There are two main models: client-side discovery, where the client queries a registry and picks an instance, and server-side discovery, where a load balancer or proxy routes requests.
| 1 | Server-side discovery with a load balancer: |
| 2 | |
| 3 | Client ──▶ API Gateway / LB ──▶ Registry ──▶ Healthy instances |
| 4 | (Consul, |
| 5 | Eureka, |
| 6 | Kubernetes DNS) |
| 7 | |
| 8 | Client-side discovery: |
| 9 | |
| 10 | Client ──▶ Registry ──▶ picks instance |
| 11 | │ |
| 12 | └── caches list, refreshes periodically |
info
Each microservice should own its data. No other service should read or write that service's tables directly. Data access happens only through the service's API. This encapsulation is what makes services independently evolvable.
The tradeoff is that operations spanning multiple services cannot use a single database transaction. You must design for eventual consistency using sagas, outbox patterns, or compensation transactions.
| 1 | Anti-pattern: shared database |
| 2 | |
| 3 | Service A ──┐ |
| 4 | ├──▶ Shared Database |
| 5 | Service B ──┘ |
| 6 | |
| 7 | Result: schema changes in one service break another. |
| 8 | |
| 9 | Pattern: database per service |
| 10 | |
| 11 | Service A ──▶ Database A |
| 12 | Service B ──▶ Database B |
| 13 | |
| 14 | Integration only through APIs or events. |
warning
Several patterns help manage the complexity of distributed systems. Knowing when to apply them separates a working microservices architecture from a fragile one.
API Gateway
An API Gateway is the single entry point for clients. It handles authentication, rate limiting, request routing, protocol translation, and response aggregation. It shields clients from the internal service topology.
Circuit Breaker
A circuit breaker stops calls to a failing service, giving it time to recover and preventing cascading failures. After a cooldown, it allows a trickle of traffic to test health before fully closing.
Saga Pattern
A saga coordinates a long-running business transaction across services using a sequence of local transactions. If one step fails, compensating transactions undo previous steps. Orchestrated sagas use a central coordinator; choreographed sagas use events.
Outbox Pattern
The outbox pattern ensures reliable event publishing. A service writes events to an outbox table in the same database transaction as the business data. A separate process reads the outbox and publishes events to a message broker.
pro tip
In a distributed system, failures are inevitable and debugging is harder. Observability — logs, metrics, and traces — is non-negotiable. Distributed tracing in particular follows a request across service boundaries, revealing latency hotspots and failure points.
| Signal | Answers | Tools |
|---|---|---|
| Logs | What happened in a service? | ELK, Loki, CloudWatch |
| Metrics | How is the system behaving over time? | Prometheus, Datadog, Grafana |
| Traces | Where did a request spend time? | Jaeger, Zipkin, Honeycomb |
warning