Microservices offer huge benefits in scalability and flexibility, but they also introduce complexity. Knowing the right patterns and avoiding anti-patterns is crucial for the success of your architecture.
Fundamental Microservices Patterns
1. API Gateway
The API Gateway acts as a single entry point for all clients:
- Routing requests to appropriate services
- Aggregating responses from multiple services
- Centralized authentication and authorization
- Rate limiting and throttling
- Protocol transformation (REST to gRPC)
- Caching frequent responses
When to use it: Always in microservices architectures. It is essential for web and mobile clients that should not know about all internal services.
2. Service Discovery
Allows services to find each other dynamically:
- Automatic registration of service instances
- Health checks to detect unavailable services
- Client-side load balancing
- Dynamic update without reconfiguration
- Integration with Consul, Eureka, or Kubernetes DNS
Popular Solutions: Consul, Eureka, etcd, Kubernetes Service Discovery
3. Circuit Breaker
Prevents cascading failures when a service is unresponsive:
- States: Closed (normal), Open (blocking calls), Half-Open (testing)
- Configurable timeouts for calls
- Failure threshold before opening circuit
- Automatic fallback to default responses
- Circuit state metrics
Libraries: Hystrix (Netflix), Resilience4j, Polly (.NET)
4. Event Sourcing
Stores state changes as a sequence of events:
- Complete history of all changes
- Ability to reconstruct state at any point
- Automatic full audit trail
- Event replay for debugging
- Multiple projections of the same event stream
Use Cases: Financial systems, e-commerce, any domain requiring full audit
5. CQRS (Command Query Responsibility Segregation)
Separates read and write operations:
- Optimized models separately
- Independent scaling of reads and writes
- Read database can be eventually consistent
- Complex queries without impacting writes
- Combines well with Event Sourcing
6. Saga Pattern
Handles distributed transactions across multiple services:
- Sequence of local transactions
- Automatic compensation in case of failure
- Two styles: Choreography (events) or Orchestration (central coordinator)
- Maintains eventual consistency
Example: Process order → Reserve inventory → Process payment → Ship order (with compensations if anything fails)
7. Strangler Fig Pattern
Gradual migration from monolith to microservices:
- Intercept requests to the monolith
- Redirect migrated functionality to new services
- Keep monolith running during migration
- Gradually "strangle" the legacy system
8. Backend for Frontend (BFF)
Specific APIs for each client type:
- Mobile BFF with data optimized for mobile
- Web BFF with data for browsers
- IoT BFF with lightweight protocols
- Reduces over-fetching and under-fetching
Anti-Patterns to Avoid
1. Distributed Monolith
The worst of both worlds: microservices complexity without the benefits:
- Highly coupled services
- Coordinated deploys required
- Sharing database between services
- Long synchronous call chains
- Duplicated business logic
Warning Signs: You can't deploy a service without updating others, changes require modifying multiple services simultaneously.
2. Nano-services
Services that are too granular:
- Excessive network overhead
- Simple operations require multiple calls
- Extremely complex debugging
- Maintenance cost not justified
Guideline: A microservice should be maintainable by a small team (2-pizza team). If your service only has 2-3 endpoints, it's probably too small.
3. Shared Database
Multiple services accessing the same database:
- Coupling at DB schema level
- Impossible to change schema without coordination
- DB resource contention
- Loss of service autonomy
- Makes technology migration difficult
Golden Rule: Each microservice must have its own database. Communication between services only via APIs.
4. Chatty Services
Excessive communication between services:
- Accumulated latency from multiple network hops
- Risk of cascading failures
- Difficult to debug and monitor
- Indicates incorrect boundaries
Solution: Re-evaluate domain boundaries. Consider grouping highly communicative services or using asynchronous events.
5. Mega Service
Service that does too much:
- Multiple bounded contexts mixed
- Large teams needed
- Risky deploys
- Makes selective scaling difficult
6. Hardcoded Endpoints
Service URLs hardcoded in the application:
- Makes dynamic scaling impossible
- Makes blue-green deploys difficult
- Does not allow automatic failover
- Problems in cloud environments
Always Use: Service discovery or configuration management (Consul, Spring Cloud Config)
7. Trying to Do Everything Right Away
Implementing all patterns from day 1:
- Over-engineering for current needs
- Unnecessary complexity
- Excessive development time
- Difficulty for team onboarding
Better Approach: Start simple, add patterns as you need them based on real problems.
Resilience Patterns
Retry with Exponential Backoff
Smart retries for transient failures:
- Wait 1s, 2s, 4s, 8s between retries
- Random jitter to avoid thundering herd
- Maximum retries configurable
- Only for recoverable errors (5xx, not 4xx)
Aggressive Timeouts
Don't wait indefinitely:
- Short timeouts for synchronous calls
- Fail fast is better than hanging indefinitely
- Allows circuit breaker to detect problems quickly
Bulkhead
Isolate resources to prevent one failure from affecting everything:
- Thread pools separated by dependent service
- Isolated connection pools
- If an external service fails, it doesn't exhaust all resources
Communication Patterns
Synchronous vs Asynchronous
Synchronous (REST, gRPC):
- Simpler to implement and debug
- Immediate response
- Strong temporal coupling
- Requires both services available
Asynchronous (Message Queue, Events):
- Temporal decoupling
- Greater resilience
- Better for long-running operations
- Complexity in debugging and monitoring
Request/Response vs Event-Driven
Use events when:
- Multiple services are interested in the same event
- You don't need an immediate response
- You want to decouple producer from consumers
Use request/response when:
- You need an immediate response
- Transactional operation
- One-to-one communication
Data Patterns
Database per Service
Each service manages its own database:
- Complete team autonomy
- Appropriate DB technology for each case
- Independent scaling
- Challenge: cross-service queries and transactions
API Composition
Combining data from multiple services:
- API Gateway aggregates data
- Data join in application layer
- Can cause performance issues
Data Replication
Replicate necessary data in each service:
- Reduces cross-service calls
- Better read performance
- Eventual consistency
- Synchronize via events
General Best Practices
Domain-Driven Design
- Identify Bounded Contexts correctly
- One microservice = One bounded context (usually)
- Ubiquitous language within each service
- Context maps for relationships between services
Observability
- Structured logging with correlation IDs
- Distributed tracing (Jaeger, Zipkin)
- Metrics with Prometheus + Grafana
- Health checks and readiness probes
Testing
- Extensive unit tests
- Integration tests with test containers
- Contract testing (Pact) between services
- Chaos engineering in production
Deployment
- CI/CD per independent service
- Containerization (Docker)
- Orchestration (Kubernetes)
- Blue-green or canary deployments
Conclusion
Microservices are not a silver bullet. They require discipline, correct patterns, and avoiding common anti-patterns. Starting with a well-structured monolith is often better than poorly designed microservices.
Patterns like API Gateway, Circuit Breaker, and Service Discovery are fundamental. Anti-patterns like Distributed Monolith or Shared Database can destroy your architecture.
At Brixato, we carefully evaluate each case. Not all projects need microservices. When they are appropriate, we apply these patterns pragmatically, adding complexity only when it brings real value.