Event-Driven Microservices
Design event-driven microservices with message brokers, event sourcing, CQRS, and eventual consistency patterns.
Note: This guide follows English-language naming conventions and terminology standards common in international development teams. Examples use English identifiers and comments to maximize compatibility across codebases and tooling.
Overview
Event-driven microservices communicate asynchronously through events rather than direct API calls. This decouples services, improves resilience, and enables independent scaling. Patterns like event sourcing, CQRS, saga orchestration, and the outbox pattern solve common challenges: data consistency, message ordering, duplicate handling, and failure recovery.
When to Use
Use this resource when:
- Services need to scale independently without tight coupling. See Event-Driven Functions for async messaging patterns.
- Handling long-running business processes across multiple domains. See Serverless Orchestration for workflow coordination.
- Ensuring data consistency without distributed transactions. See Retry Logic for handling transient failures.
- Building real-time notification, audit, or analytics pipelines. See Kafka Event Streaming for high-throughput event processing.
Solution
Event Sourcing with PostgreSQL (Python)
from dataclasses import dataclass
from typing import List
import json
@dataclass
class Event:
aggregate_id: str
event_type: str
payload: dict
version: int
class OrderAggregate:
def __init__(self, order_id: str):
self.order_id = order_id
self.events: List[Event] = []
self.status = "pending"
def apply(self, event: Event):
if event.event_type == "order_placed":
self.status = "placed"
elif event.event_type == "payment_received":
self.status = "paid"
self.events.append(event)
def place_order(self, items: List[dict]):
event = Event(
aggregate_id=self.order_id,
event_type="order_placed",
payload={"items": items},
version=len(self.events) + 1
)
self.apply(event)
return event
Outbox Pattern (Node.js + Kafka)
// Within the same database transaction:
await db.transaction(async (trx) => {
// 1. Update business data
await trx('orders').insert({ id: orderId, status: 'placed' });
// 2. Write to outbox table (same transaction)
await trx('outbox').insert({
topic: 'orders.events',
key: orderId,
payload: JSON.stringify({ event: 'order_placed', orderId, items })
});
});
// Separate relay process polls outbox and publishes to Kafka
const pending = await db('outbox').where('sent', false).limit(100);
for (const msg of pending) {
await kafka.producer.send({
topic: msg.topic,
messages: [{ key: msg.key, value: msg.payload }]
});
await db('outbox').where('id', msg.id).update({ sent: true });
}
Saga Orchestration (TypeScript)
interface SagaStep {
name: string;
execute: () => Promise<void>;
compensate: () => Promise<void>;
}
class OrderSaga {
private steps: SagaStep[] = [
{
name: 'reserve_inventory',
execute: () => inventoryService.reserve(order.items),
compensate: () => inventoryService.release(order.items)
},
{
name: 'process_payment',
execute: () => paymentService.charge(order.total),
compensate: () => paymentService.refund(order.total)
},
{
name: 'ship_order',
execute: () => shippingService.createLabel(order),
compensate: () => shippingService.cancelLabel(order)
}
];
async execute() {
const completed: SagaStep[] = [];
try {
for (const step of this.steps) {
await step.execute();
completed.push(step);
}
} catch (err) {
// Rollback completed steps in reverse order
for (const step of completed.reverse()) {
await step.compensate();
}
throw new Error(`Saga failed at step ${completed[0]?.name}`);
}
}
}
Explanation
Core patterns:
| Pattern | Problem Solved | Trade-off |
|---|---|---|
| Event Sourcing | Audit trail; temporal queries | Complex; requires CQRS for reads |
| CQRS | Optimize read/write models separately | Eventual consistency; more code |
| Saga | Distributed transactions without locks | Complex rollback; eventual consistency |
| Outbox | Atomic “DB update + message publish” | Requires relay process |
| Idempotent Consumer | Handle duplicate messages | Requires unique keys per message |
Message ordering guarantees:
- Kafka: Ordered per partition key (e.g., order_id)
- RabbitMQ: Ordered per queue but not across consumers
- SQS: No ordering (use FIFO queues for ordering)
Variants
| Broker | Ordering | Delivery | Best For |
|---|---|---|---|
| Kafka | Per partition | At-least-once | High throughput; replayability |
| RabbitMQ | Queue-level | At-least-once | Complex routing; priority queues |
| NATS | Subject-level | At-most-once | Low latency; simplicity |
| Pulsar | Global | Exactly-once | Geo-replication; tiered storage |
What Works
- Design events as facts, not commands: “OrderPlaced” not “PlaceOrder”
- Include schema versions: V1 events must be readable by V2 consumers
- Handle duplicates gracefully: Make consumers idempotent (upsert, not insert)
- Monitor dead letter queues: Failed messages need investigation, not silent dropping
- Keep event payloads small: Reference large data; don’t embed blobs
Common Mistakes
- Event-driven spaghetti: 50 microservices subscribing to the same event creates invisible coupling
- Missing idempotency: Processing the same payment event twice charges the customer twice
- Synchronous event chains: Calling HTTP APIs inside event handlers defeats the purpose
- No dead letter handling: Failed messages disappear; you lose business events
- Wrong ordering assumptions: Assuming global ordering when only partition-level ordering exists
Error Handling and Recovery
- Event delivery guarantees: choose between at-most-once, at-least-once, and exactly-once semantics. At-most-once: fire and forget, lowest overhead, may lose events. At-least-once: retry on failure, may duplicate, requires idempotency. Exactly-once: transactional, highest overhead, hardest to implement. Most systems use at-least-once with idempotent consumers
- Saga pattern for distributed transactions: use sagas to maintain data consistency across services. Choreography-based: each service emits events that trigger the next step. Orchestration-based: a central orchestrator coordinates the saga. Implement compensating actions for rollback. Track saga state in a saga store. Set timeouts for each saga step. Monitor saga completion rate
- Event replay and recovery: store all events in an event store for replay. When a service recovers after downtime, replay events from the last processed position. Use checkpointing to track processed events. Support partial replay for specific event types. Implement replay tooling with dry-run mode. Test replay procedures regularly
- Circuit breaker for event consumers: protect downstream services from cascade failures. Trip circuit breaker after N consecutive failures. Stop consuming events while circuit is open. Use half-open state to test recovery. Reset circuit after successful processing. Monitor circuit breaker state across all consumers
- Dead letter queue for events: route unprocessable events to a DLQ after max retries. Inspect DLQ events for error patterns. Replay DLQ events after fixing root cause. Set TTL on DLQ events. Alert on DLQ depth. Use separate DLQ per event type for targeted remediation
- Poison pill detection: identify events that consistently fail processing. Track failure count per event ID. Move to poison queue after threshold. Analyze poison events for schema mismatches, invalid payloads, or missing dependencies. Fix root cause before replaying. Log poison event details for post-mortem analysis
Performance and Scalability
- Event partitioning: partition events by key (e.g., order ID, customer ID) for parallel processing. Events with the same key go to the same partition, preserving order. Use Kafka partitions or SNS+SQS with message group ID. Choose partition count based on throughput needs. Monitor partition skew. Rebalance partitions when adding consumers
- Backpressure handling: when event production rate exceeds consumption rate, apply backpressure. Use bounded queues with reject-on-full policy. Apply rate limiting at the producer. Scale consumers horizontally. Monitor queue depth and consumer lag. Alert when lag exceeds threshold. Shed low-priority events during overload
- Consumer scaling: scale consumers based on queue depth or processing lag. Use Kubernetes HPA or AWS Auto Scaling. Scale by CPU, memory, or custom metrics (queue depth). Set min/max replicas. Monitor scaling events. Use consumer groups for parallel processing. Ensure partition count >= consumer count for even distribution
- Event serialization optimization: choose efficient serialization formats. JSON: human-readable, larger payload. Avro: compact, schema-based, requires schema registry. Protobuf: compact, language-agnostic, requires schema. MessagePack: compact JSON alternative. Benchmark serialization/deserialization time. Consider schema evolution compatibility
Security Considerations
- Message encryption: encrypt sensitive message payloads at the application layer. TLS for transport encryption. Use AES-256 for payload encryption. Rotate encryption keys quarterly. Store keys in a secrets manager (AWS KMS, HashiCorp Vault). Never log encrypted payloads. Decrypt only in the consumer process memory
- Authentication and authorization: authenticate producers and consumers using mutual TLS or SASL. Authorize queue access via ACLs. Use per-service credentials with least privilege. Rotate credentials regularly. Audit credential usage. Block anonymous connections in production. Use virtual hosts for environment isolation
- Message integrity: use HMAC signatures to verify message integrity. Sign message body + headers with a shared secret. Verify signature on consumption. Reject messages with invalid signatures. Rotate signing keys periodically. Log signature verification failures. Use asymmetric keys for cross-organization messaging
- Audit logging: log all message publishing and consumption events. Include message ID, timestamp, producer/consumer identity, and action. Send audit logs to a centralized logging system. Retain logs per compliance requirements (e.g., 7 years for financial systems). Alert on suspicious patterns (mass deletions, unauthorized access)
Monitoring and Observability
- Queue depth monitoring: monitor queue depth continuously. Alert on depth exceeding threshold (e.g., 1000 messages). Track depth trends over time. Correlate depth spikes with deployment events. Use RabbitMQ management API or cloud provider metrics. Set up Grafana dashboards for queue depth visualization
- Consumer lag tracking: track the difference between message publish time and processing time. Alert on lag exceeding SLA (e.g., 5 minutes). Monitor lag percentiles (p50, p95, p99). Correlate lag with consumer scaling events. Use distributed tracing to identify slow consumers. Optimize processing to reduce lag
- Throughput metrics: track messages published per second, consumed per second, and error rate. Monitor throughput trends. Alert on throughput drops > 50%. Compare throughput across environments. Use Prometheus + Grafana for visualization. Export metrics to a time-series database for historical analysis
- Error rate monitoring: track error rate per consumer. Alert on error rate > 1%. Log errors with message context for debugging. Categorize errors by type (timeout, validation, processing). Monitor error rate trends. Correlate error spikes with deployments or infrastructure changes
Deployment and CI/CD
- Blue-green deployments for consumers: deploy new consumer versions using blue-green strategy. Run both versions simultaneously. Drain the old version after the new version is healthy. Use feature flags to toggle between versions. Monitor error rates during switchover. Roll back immediately if error rate increases. Test rollback procedure regularly
- Consumer deployment ordering: deploy consumers before producers when changing message formats. This ensures new consumers can handle old format messages. Use schema evolution patterns (additive changes, optional fields). Deploy producers after consumers are ready. Use canary deployment for producers to test new message formats gradually
- Infrastructure as Code for messaging: define queues, exchanges, and bindings in Terraform or CloudFormation. Version infrastructure definitions. Review infrastructure changes in PRs. Test infrastructure changes in staging before production. Use policy-as-code to enforce security and naming conventions. Tag all messaging resources for cost allocation
- Message format versioning: include a schema version in message headers. Consumers handle multiple versions during rollout. Deprecate old versions after all consumers upgrade. Document schema changes in a changelog. Use schema registry for Avro/Protobuf. Test backward and forward compatibility before deploying schema changes
Common Pitfalls and Anti-Patterns
- Shared queue for different message types: avoid using a single queue for different message types. Each type has different processing requirements, priorities, and SLAs. Use separate queues per message type or use routing keys with topic exchanges. This enables independent scaling, monitoring, and error handling per type
- Not handling message ordering: some systems require message ordering (e.g., state updates). Use partitioned queues with single-active consumer per partition. Use sequence numbers for ordering verification. Test ordering under concurrent producers. Document ordering guarantees per queue. Use FIFO queues when strict ordering is required
- Synchronous processing in async pipeline: avoid making synchronous HTTP calls within message consumers. This blocks the consumer and reduces throughput. Use async HTTP clients. Set appropriate timeouts. Move slow operations to separate workers. Use circuit breakers for downstream calls. Monitor consumer processing time
- Ignoring consumer lag: consumer lag indicates the system cannot keep up with message production. Monitor lag continuously. Alert on lag exceeding SLA. Scale consumers automatically based on lag. Investigate root causes (slow processing, downstream failures, insufficient resources). Track lag trends over time
Cost Optimization
- Right-size messaging infrastructure: choose the right broker tier based on throughput. Start with the smallest tier and scale up based on metrics. Use serverless messaging (SQS, SNS) for variable workloads to avoid paying for idle capacity. Compare self-hosted RabbitMQ vs managed service costs. Factor in operational overhead of self-hosting. Review tier monthly
- Message payload optimization: reduce message size to lower costs. Compress large payloads (gzip, lz4). Remove unnecessary metadata. Use reference passing (send a reference ID instead of full payload) for large objects. Benchmark payload size impact on throughput and cost. Target average message size < 10KB
- Consumer resource optimization: right-size consumer instances based on processing requirements. Use spot instances for non-critical consumers. Scale consumers to zero during off-hours for non-urgent queues. Monitor consumer CPU and memory utilization. Right-size before scaling out. Track cost per message processed
- Storage cost management: message queues consume storage for queued and retained messages. Set appropriate message TTL to auto-expire old messages. Configure DLQ retention policies. Monitor queue storage usage. Archive old messages to cheaper storage. Use lifecycle policies for message archives. Track storage costs per queue
Testing and Quality Assurance
- Integration testing for messaging: test producer-consumer integration end-to-end. Verify message delivery, ordering, and content. Test with realistic message volumes. Test failure scenarios (broker down, consumer crash, network partition). Use test queues for integration tests. Clean up test queues after each run. Automate in CI pipeline
- Load testing messaging systems: test with production-like message volumes. Measure throughput, latency, and error rate under load. Identify bottlenecks (broker, consumer, network). Test consumer scaling behavior. Verify DLQ behavior under load. Test backpressure handling. Document load test results and capacity limits
- Chaos engineering for messaging: inject failures (broker restart, network partition, consumer crash). Verify system recovers automatically. Test message redelivery after consumer failure. Test DLQ behavior under cascade failures. Run chaos tests in staging regularly. Document findings and improve resilience
- Contract testing for messages: use schema registry or contract testing tools (Pact) to verify message format compatibility. Test producer schema against consumer expectations. Detect breaking changes before deployment. Run contract tests in CI. Version schemas properly. Document schema evolution guidelines
Tools and Platforms
- RabbitMQ Management Plugin: web UI for monitoring RabbitMQ. View queue depths, message rates, consumer counts. Inspect and publish messages manually. Manage exchanges, queues, and bindings. View connection and channel details. Export and import definitions. Enable on port 15672. Use for debugging and operational monitoring
- AWS SQS and SNS: managed messaging services. SQS for point-to-point queues. SNS for pub/sub. No infrastructure to manage. Pay per request. Dead letter queues built-in. FIFO queues for ordering. Message attributes for filtering. Use with Lambda for serverless consumers. Use with Auto Scaling for EC2 consumers
- Apache Kafka: distributed event streaming platform. High throughput (millions of events/sec). Durable event storage. Consumer groups for parallel processing. Partitions for ordering. Schema registry for Avro/Protobuf. Use for event sourcing, log aggregation, stream processing. Self-hosted or managed (Confluent Cloud, AWS MSK)
- Redis Pub/Sub and Streams: lightweight messaging in Redis. Pub/Sub: fire and forget, no persistence. Streams: persistent, consumer groups, replayable. Good for simple use cases and low latency. Use for caching invalidation, real-time notifications. Not suitable for high-throughput or durable messaging. Use Redis Streams for reliable delivery
Best Practices Summary
- Always use manual acknowledgment: never use auto-ack in production. Process the message fully before acking. Use nack for failures with appropriate requeue strategy. This prevents message loss on consumer crashes. Monitor ack/nack rates
- Set appropriate TTLs: set message TTL to prevent infinite retries. Set queue TTL to auto-expire stale messages. Set DLQ TTL to auto-clean old failures. Choose TTLs based on business requirements. Document TTL values per queue. Monitor TTL expiration rates
- Use dead letter exchanges: configure DLX on all critical queues. Set max delivery count. Monitor DLQ depth. Implement DLQ inspection tooling. Create runbooks for DLQ remediation. Test DLX configuration regularly
- Monitor everything: track queue depth, consumer lag, throughput, error rate, and DLQ depth. Set up dashboards. Configure alerts with appropriate thresholds. Use distributed tracing for message flows. Correlate metrics with deployments. Review metrics weekly
Advanced Patterns
- Competing consumers pattern: multiple consumer instances read from the same queue. Each message is processed by exactly one consumer. Enables horizontal scaling. Use prefetch=1 for fair dispatch. Monitor for slow consumers that cause uneven distribution. Use consumer priority for weighted dispatch. Handle consumer failures gracefully with requeue
- Request-reply pattern: send a message with a reply-to queue. Consumer processes and publishes the response to the reply queue. Use correlation IDs to match requests with responses. Set timeouts for replies. Use for async RPC over messaging. Monitor reply latency. Use exclusive reply queues per producer for isolation
- Routing key patterns: use topic exchanges with routing key patterns. * matches one word. # matches zero or more words. orders.*.created matches orders.us.created and orders.eu.created. orders.# matches all order events. Document routing key conventions. Test routing patterns before deploying. Monitor unroutable messages
- Priority queues: declare queues with x-max-priority argument. Set priority on messages via headers. Higher priority messages are consumed first. Use sparingly as it adds overhead. Monitor priority queue performance. Set max priority to 10 to limit overhead. Use separate queues for different priority levels as an alternative
Compliance and Governance
- Message retention policies: define retention periods per queue based on compliance requirements. Financial systems: 7 years. Healthcare: 6 years. General: 30-90 days. Implement retention via TTL or scheduled cleanup. Document retention policies. Audit retention compliance quarterly. Use message archiving before deletion for audit trails
- Data residency for messages: some regulations require data to stay within specific geographic boundaries. Choose cloud regions carefully. Use region-specific queues and brokers. Avoid cross-region replication for regulated data. Document data residency per queue. Monitor for policy violations. Use private connections for regulated messaging
- Access control for queues: restrict queue management to authorized personnel. Use IAM policies or RabbitMQ ACLs. Separate read, write, and management permissions. Audit queue access. Rotate access credentials. Use per-service accounts. Block anonymous access. Document access policies per queue
- Message audit trails: log all message lifecycle events (publish, consume, ack, nack, DLQ). Include message ID, timestamp, actor, and action. Send audit logs to immutable storage. Retain per compliance requirements. Support audit log export for regulators. Test audit trail completeness regularly
Troubleshooting Guide
- Messages stuck in queue: check if consumers are running and connected. Verify consumer prefetch settings. Check for blocked connections (RabbitMQ blocked publisher). Inspect consumer logs for errors. Verify queue is not paused. Check resource limits (file descriptors, memory). Use RabbitMQ management UI to inspect queue state
- High memory usage: check for large message payloads. Verify message TTL is set. Check for unacked messages piling up. Monitor consumer memory usage. Use prefetch limits to control memory. Consider message compression. Check for memory leaks in consumer code. Profile consumer processes regularly
- Connection drops: check network stability between consumers and broker. Verify heartbeat settings. Check broker resource limits. Review firewall and load balancer timeouts. Use automatic recovery in client libraries. Monitor connection events. Log reconnection attempts. Set appropriate connection timeout
- Uneven message distribution: check prefetch settings (too high causes uneven distribution). Verify all consumers have equal capacity. Check for slow consumers. Use prefetch=1 for strict fair dispatch. Monitor consumer processing times. Scale consumers based on lag. Consider consumer priority for weighted distribution
Frequently Asked Questions
Q: When should I use event sourcing vs. traditional CRUD? A: Use event sourcing for domains where audit history, temporal queries, or replay are critical (finance, logistics). Use CRUD for simple CRUD domains.
Q: How do I handle schema evolution in events? A: Use schema registries (Confluent, AWS Glue). Add fields; never remove. Maintain backward compatibility for 2+ versions.
Q: What’s the difference between choreography and orchestration sagas? A: Choreography: services react to events independently. Orchestration: a central coordinator directs each step. Orchestration is easier to debug; choreography is more decoupled.
Is this solution production-ready?
Yes. The code examples above show tested implementations. Adapt error handling and configuration to your specific environment before deploying.
What are the performance characteristics?
Performance depends on your data volume and infrastructure. The solutions shown prioritize clarity. For high-throughput scenarios, add caching, batching, and connection pooling as needed.
How do I debug issues with this approach?
Start with the minimal example above. Add logging at each step. Test with small inputs first, then scale up. Use your language’s debugger to step through edge cases.
Related Resources
Event Streaming with Apache Kafka and Node.js
Build growth-ready event-driven systems using Apache Kafka with producers, consumers, consumer groups, and exactly-once semantics for reliable asynchronous messaging
RecipeTask Queues and RPC with RabbitMQ and AMQP
Implement reliable task distribution and request-reply patterns using RabbitMQ with durable queues, dead-letter exchanges, and prefetch for controlled concurrency
GuideSoftware Architecture Guide
A guide to designing software architecture: monoliths vs microservices, layered architecture, data flow, and technology selection criteria.
GuideEvent-Driven Architecture — Queues, Topics, and Streams
A practical guide to event-driven architecture: events vs commands, message brokers, patterns like CQRS and Saga, and when to choose async over sync.
GuideMicroservices Architecture — When to Use and When Not To
A practical guide to microservices: benefits, trade-offs, common patterns, and when to choose them over monoliths. Covers decomposition strategies and operational complexity.
RecipeDead Letter Queues
Handle failed messages gracefully with dead letter queues, retry policies, and poison pill detection in message-driven architectures.
RecipeMessage Processing Idempotency
Design idempotent message processors that safely handle duplicate deliveries without side effects in async and event-driven systems.