Microservice architecture has become the dominant approach for teams building systems that need to scale independently, deploy without downtime, and evolve different components at different rates. But moving from a monolith to microservices β or designing them correctly from the start β requires understanding a specific set of patterns that distinguish production-ready distributed systems from ones that collapse under real load or operational pressure.
These are the patterns that experienced distributed systems engineers rely on β and the anti-patterns that catch unprepared teams by surprise.
When Monoliths Break Down (and Where Microservices Actually Help)
A well-structured monolith is not inherently inferior to microservices. For small teams and early-stage products, a monolith is almost always the right call β simpler to deploy, debug, and reason about. Microservices introduce real operational complexity: distributed tracing, network partitions, eventual consistency, and service discovery all become your responsibility.
The switch makes sense when:
- Deployment velocity is blocked β a change to the payment module forces the entire application to deploy, slowing releases across independent teams.
- Scaling is uneven β the image processing service needs 50 instances and the auth service needs 2, but both must scale together in a monolith.
- Team autonomy requires it β Conway's Law forces architectural seams along organizational boundaries.
- Fault isolation matters β a bug in one component must not cascade into a full outage.
If none of these apply to your system today, reconsider whether microservices are the right fit now. Premature decomposition is expensive to reverse.
The Database-Per-Service Pattern
The most fundamental microservices rule β and the most commonly violated β is that each service owns its own data store. No two services share a database schema. This is not a suggestion; it is the load-bearing wall of the entire architecture.
Shared databases create invisible coupling: a schema change in one service breaks another. Services can't choose the right persistence technology for their workload (relational, document, time-series, graph). You cannot deploy services independently if their migrations step on each other.
Image: Microservice Databases with a new service β Xiaoan888 (CC BY-SA 4.0), via Wikimedia Commons
The challenge this introduces is data consistency across services. When a user places an order (Order Service) and you need to update inventory (Inventory Service), you can't use a single database transaction. This is precisely what the Saga pattern exists to solve.
The API Gateway Pattern
Clients shouldn't need to know the internal topology of your services β nor should they be forced to make a dozen network calls to render a single page. An API Gateway sits in front of your microservices and handles cross-cutting concerns centrally:
- Request routing β directing each request to the appropriate backend service based on path, header, or content
- Authentication and authorization β verifying JWT tokens once at the edge rather than in every service
- Rate limiting β protecting backend services from traffic spikes and abuse
- Request composition β aggregating responses from multiple services into a single response (the Backend for Frontend pattern)
- SSL termination β centralizing TLS handling and certificate management
Production implementations typically use purpose-built gateways β Kong, AWS API Gateway, Nginx, or Envoy β rather than hand-rolling this functionality. The Backend for Frontend (BFF) variant creates a dedicated gateway for each client type (mobile, web, internal tools) to avoid one-size-fits-all endpoints that serve each client poorly.
The Circuit Breaker Pattern
In a distributed system, one slow or failing service can cascade into a full outage through a chain of timeouts and thread exhaustion. The Circuit Breaker pattern prevents this by detecting failure and stopping the flow of requests to a failing dependency before threads pile up waiting for responses that won't come.
A circuit breaker operates in three states:
- Closed: Requests flow normally. The breaker monitors failure rates and latency.
- Open: The breaker has tripped. Requests fail immediately with a fallback (cached response, degraded feature, or a clear error) β no attempt is made to call the failing service.
- Half-Open: After a cooldown period, a single probe request is allowed through. If it succeeds, the circuit closes. If it fails, it opens again.
Libraries like Resilience4j (Java), Polly (.NET), and PyBreaker (Python) implement this at the application level. Service meshes like Istio and Linkerd implement circuit breaking at the infrastructure layer, removing it from application code entirely β a significant operational advantage in polyglot environments.
The Saga Pattern for Distributed Transactions
ACID transactions don't cross service boundaries. When a business operation spans multiple services β order creation, inventory reservation, payment processing, notification dispatch β you need a strategy for handling partial failures without leaving your system in an inconsistent state.
The Saga pattern decomposes a multi-service transaction into a sequence of local transactions, each publishing an event or message that triggers the next step. On failure, compensating transactions roll back the steps that already succeeded.
Two implementation styles exist:
- Choreography: Services react to each other's events via an event bus (Kafka, RabbitMQ, Amazon EventBridge). No central coordinator. This works well for simple, linear flows but becomes extremely difficult to trace and debug when flows are complex or have many branches.
- Orchestration: A central Saga Orchestrator service explicitly commands each step and handles compensations. Easier to trace, visualize, and debug. Tools like Temporal, AWS Step Functions, and Netflix Conductor make this pattern production-ready. The coordinator itself must be made resilient through event sourcing or durable execution.
| Pattern | Problem Solved | Key Trade-off | Production Tooling |
|---|---|---|---|
| Database-per-Service | Schema coupling, independent scaling | Requires eventual consistency strategy | Per-service: Postgres, Redis, MongoDB |
| API Gateway | Client coupling, cross-cutting concerns | Gateway can become a single point of failure | Kong, AWS API Gateway, Nginx, Envoy |
| Circuit Breaker | Cascading failures, thread exhaustion | Requires careful threshold tuning | Resilience4j, Polly, Istio, Linkerd |
| Saga (Choreography) | Cross-service transactions (simple flows) | Hard to trace; complex failure paths | Kafka, RabbitMQ, Amazon EventBridge |
| Saga (Orchestration) | Cross-service transactions (complex flows) | Central coordinator adds overhead | Temporal, AWS Step Functions, Conductor |
| Service Mesh | Cross-cutting reliability at infra level | Significant operational complexity | Istio, Linkerd, Consul Connect |
Service Mesh: Infrastructure-Level Resilience
A service mesh moves cross-cutting concerns β circuit breaking, retries, mutual TLS, distributed tracing, traffic management β out of application code and into a dedicated infrastructure layer. Each service gets a sidecar proxy (typically Envoy) deployed alongside it, and these proxies handle inter-service communication transparently.
The appeal is real: your Go service doesn't need a retry library. Your Python service doesn't implement circuit breaking. The mesh handles it uniformly across every language and framework in your ecosystem, and you get consistent observability across all services via OpenTelemetry.
The cost is also real. Istio, in particular, has historically added significant operational complexity β control plane configuration can be intricate, and debugging mTLS or routing issues requires understanding the mesh's own data model. Linkerd is considerably simpler to operate and is often a better starting point for teams new to service meshes. Evaluate whether your cross-cutting requirements genuinely justify the operational overhead before committing.
Frequently Asked Questions
When should you not use microservices?
When your team is small (fewer than approximately 8β10 engineers), when you're still finding product-market fit and need to iterate rapidly, or when your system's domain boundaries aren't yet clear. Premature microservice decomposition along the wrong boundaries is expensive to reverse. Start with a well-structured modular monolith, then extract services when you have concrete scaling or deployment independence requirements β not before.
How do you handle authentication between services?
Mutual TLS (mTLS) is the strongest option at the infrastructure level, particularly within a service mesh. Application-level options include short-lived service tokens from an identity provider (HashiCorp Vault, SPIFFE/SPIRE), scoped JWT tokens validated at the API Gateway, or network-level isolation where services communicate only within a private VPC. The right choice depends on your threat model and operational maturity.
What is the most common microservices mistake teams make?
Sharing a database. It is the single most common architectural mistake and completely undermines the core benefit of microservices: the ability to deploy, scale, and evolve services independently. The second most common mistake is underestimating observability needs β distributed tracing (via OpenTelemetry), structured logging correlated by trace ID, and health check endpoints are not optional in a distributed system. Without them, debugging production incidents becomes extremely difficult.
Successful microservice architectures in production share a common foundation: strict data ownership through the database-per-service pattern, an API Gateway handling edge concerns, Circuit Breakers preventing cascading failures, and either a Saga pattern or a service mesh for distributed transaction management. We recommend introducing these patterns incrementally and in proportion to your actual complexity β validate each one in production before adding the next layer.
Sources & References:
Chris Richardson, Microservices.io β Pattern Library for Microservices Architecture
Martin Fowler & James Lewis, "Microservices" β martinfowler.com
OpenTelemetry Project, OpenTelemetry Documentation
Disclaimer: This article is for informational purposes only. Technology landscapes change rapidly; verify information with official sources before making technical decisions.