Microservices architecture is not a single pattern — it is a collection of patterns that work together to create scalable, resilient distributed systems. After designing architectures for over 50 enterprise migrations, these are the patterns we use most frequently and the real-world tradeoffs of each.
API Gateway Pattern
The API Gateway sits at the edge of your microservices architecture, acting as the single entry point for all client requests. It handles cross-cutting concerns like authentication, rate limiting, request routing, and protocol translation.
In practice, we implement API Gateways using Kong, Ambassador, or AWS API Gateway depending on the client infrastructure. The gateway routes requests to the appropriate microservice, aggregates responses when a client needs data from multiple services, and enforces security policies consistently across all endpoints.
The key tradeoff is that the gateway becomes a single point of failure and a potential bottleneck. Mitigate this with horizontal scaling, health checks, and circuit breakers. Never put business logic in the gateway — it should be a thin routing and policy layer.
Database-per-Service Pattern
Each microservice owns its data store. The order service has its own PostgreSQL database. The product catalog uses Elasticsearch. The session service uses Redis. No service directly accesses another service database — ever.
This is the pattern teams resist most because it feels wasteful. Why run five databases when one would do? The answer is autonomy and isolation. When the product team wants to migrate from PostgreSQL to MongoDB, they can do it without coordinating with every other team. When the order database goes down, the product catalog is unaffected.
The challenge is maintaining data consistency across services. You lose ACID transactions that span multiple services. Instead, you use eventual consistency patterns like the Saga pattern or event-driven synchronization.
Event-Driven Architecture
Instead of services calling each other synchronously via HTTP, they communicate through events. When a customer places an order, the order service publishes an OrderPlaced event. The inventory service, the notification service, and the analytics service each consume that event independently.
We implement this with Apache Kafka for high-throughput systems (millions of events per second) or RabbitMQ for simpler message routing needs. The event log becomes the source of truth — services can replay events to rebuild their state, and new services can be added without modifying existing ones.
Event-driven architectures are powerful but add complexity: event schema evolution, exactly-once delivery guarantees, event ordering, and dead letter queues all need careful design. We use schema registries (Confluent or AWS Glue) and consumer group management to handle these challenges.
Saga Pattern
When a business transaction spans multiple services, you cannot use a traditional database transaction. The Saga pattern breaks the transaction into a sequence of local transactions, each with a compensating action for rollback.
Consider an e-commerce checkout: Reserve inventory → Charge payment → Create shipment. If the payment fails, the saga executes compensating actions: Release inventory. If the shipment creation fails: Refund payment → Release inventory.
We implement sagas in two ways:
- Choreography: Each service listens for events and decides what to do next. Simple to implement but hard to debug and monitor as the number of services grows.
- Orchestration: A central orchestrator coordinates the saga steps. Easier to understand and monitor, but the orchestrator becomes a coupling point. We prefer orchestration for complex business processes with more than three steps.
Circuit Breaker Pattern
In a distributed system, one slow or failing service can cascade and bring down everything. The circuit breaker pattern detects failures and stops sending requests to a failing service, allowing it to recover.
The circuit breaker has three states: Closed (normal operation), Open (requests fail immediately without calling the downstream service), and Half-Open (a few test requests are allowed through to check if the service has recovered).
We implement circuit breakers at the service mesh level using Istio or Linkerd, which means application code does not need to implement the pattern directly. The service mesh handles retries, timeouts, and circuit breaking transparently.
Sidecar Pattern
The sidecar pattern deploys a helper container alongside your application container in the same pod. The sidecar handles cross-cutting concerns — logging, monitoring, TLS termination, traffic management — without modifying the application code.
Service meshes like Istio and Linkerd use the sidecar pattern extensively. An Envoy proxy sidecar intercepts all network traffic to and from your application, providing mutual TLS, observability, and traffic control. Your application just makes plain HTTP calls — the sidecar handles encryption, retries, and circuit breaking.
Strangler Fig Pattern
This is the migration pattern we use most frequently. Named after the strangler fig tree that grows around its host, this pattern incrementally replaces monolith functionality with microservices.
You place a facade (usually the API gateway) in front of the monolith. New features are built as microservices. Existing features are gradually extracted. The monolith shrinks over time until it can be decommissioned entirely — or kept as a legacy service handling only the functionality that is not worth extracting.
The beauty of this pattern is that it eliminates the risk of a big-bang rewrite. The monolith continues to serve traffic throughout the migration. If a new microservice has issues, traffic can be routed back to the monolith instantly.
CQRS (Command Query Responsibility Segregation)
CQRS separates read and write operations into different models. The write model handles commands (create order, update profile) and the read model handles queries (list orders, search products). Each model can be optimized independently.
This pattern shines when read and write patterns are dramatically different — which is the case for most applications. An e-commerce product catalog might receive 1,000 reads for every write. With CQRS, you can scale the read model independently, use a different data store optimized for queries (Elasticsearch for search, Redis for hot data), and keep the write model simple and consistent.
Choosing the Right Patterns
No architecture uses all of these patterns. The right combination depends on your domain complexity, team size, consistency requirements, and performance characteristics. We have seen teams over-engineer by applying every pattern from day one. Start simple, measure, and add patterns when you have evidence they are needed.
If you are designing a microservices architecture and want expert guidance on pattern selection, book a free architecture review. We will analyze your specific requirements and recommend the patterns that will deliver the most value for your system.