Event-driven architecture (EDA) is one of the most powerful — and most over-applied — architectural patterns. When used correctly, it enables loose coupling, independent scalability, and real-time responsiveness. When used incorrectly, it introduces debugging nightmares, eventual consistency headaches, and unnecessary complexity.
Here's how to use EDA effectively.
What Event-Driven Architecture Is
In traditional request-response architecture, services call each other directly. Service A sends a request to Service B, waits for a response, and continues.
In event-driven architecture, services communicate by producing and consuming events. Service A publishes an event ("order placed"). Service B, C, and D consume that event independently and act on it — without Service A knowing or caring what they do with it.
The fundamental shift: From "tell a specific service to do something" to "announce that something happened and let interested services react."
Event Types
Domain Events
Facts about something that happened in your business domain. Immutable. Past tense.
Examples: OrderPlaced, PaymentProcessed, UserRegistered, InventoryReserved
Characteristics: Named as past-tense verbs. Contain the relevant data at the time of the event. Cannot be changed after emission.
Integration Events
Events published specifically for cross-service communication. Similar to domain events but designed for external consumers.
Commands
Requests for a specific action. Future tense. Directed at a specific service.
Examples: ProcessPayment, SendNotification, ReserveInventory
Key difference from events: Commands are directed at a specific consumer. Events are broadcast to all interested consumers.
Patterns
Pattern 1: Event Notification
The simplest pattern. A service publishes a lightweight event (type + ID, minimal data). Consumers query back for full details if needed.
Example:
{
"type": "OrderPlaced",
"orderId": "ord-123",
"timestamp": "2026-05-04T10:30:00Z"
}
Pros: Loose coupling. Events are small. Simple to implement. Cons: Consumers need to call back for data (extra network calls, coupling through queries).
Pattern 2: Event-Carried State Transfer
Events carry all the data the consumer needs. No callback required.
Example:
{
"type": "OrderPlaced",
"orderId": "ord-123",
"customerId": "cust-456",
"items": [{"sku": "WIDGET-1", "quantity": 2, "price": 29.99}],
"total": 59.98,
"timestamp": "2026-05-04T10:30:00Z"
}
Pros: No callbacks needed. Consumers are fully independent. Better for high-throughput systems. Cons: Larger events. Data duplication across services. Risk of stale data if consumers cache event data.
Pattern 3: Event Sourcing
Instead of storing current state, store the sequence of events that led to the current state. The current state is derived by replaying events.
Example: An account's balance isn't stored as a number. It's calculated by replaying all MoneyDeposited and MoneyWithdrawn events.
Pros: Complete audit trail. Can reconstruct state at any point in time. Natural fit for financial and compliance applications. Cons: Complex to implement. Read performance requires projections. Mental model shift for developers.
Pattern 4: CQRS (Command Query Responsibility Segregation)
Separate the write model (commands) from the read model (queries). Often combined with event sourcing.
When it works: When read and write patterns are fundamentally different (e.g., writes are transactional, reads are analytical). When you need different data models optimised for different query patterns.
When it's overkill: Simple CRUD applications. Systems where read and write patterns are similar.
Technology Choices
| Technology | Type | Strengths | Best For |
|---|---|---|---|
| Apache Kafka | Distributed log | High throughput, durability, replay | High-volume event streaming |
| Azure Event Hubs | Managed streaming | Azure-native, easy setup, Kafka-compatible | Azure-centric, managed |
| AWS EventBridge | Serverless event bus | Schema registry, filtering, routing | AWS-centric, serverless |
| RabbitMQ | Message broker | Flexible routing, simple, mature | Moderate volume, complex routing |
| Azure Service Bus | Enterprise messaging | Transactions, sessions, dead-letter | Enterprise messaging patterns |
| Redis Streams | In-memory streaming | Low latency, simple | Low-latency, moderate volume |
| NATS | Cloud-native messaging | Lightweight, fast, simple | Microservices, cloud-native |
Decision Framework
- Need replay capability? → Kafka, Event Hubs
- Need complex routing? → RabbitMQ, Service Bus
- Need serverless integration? → EventBridge
- Need lowest latency? → Redis Streams, NATS
- Need enterprise features (transactions, dead-letter)? → Service Bus, RabbitMQ
Schema Evolution
As events evolve over time, you need backward and forward compatibility:
Rules for safe evolution:
- You CAN add new optional fields
- You CANNOT remove fields that consumers depend on
- You CANNOT change field types
- You CAN add new event types
Schema Registry: Use a schema registry (Confluent Schema Registry, Azure Schema Registry) to enforce compatibility. Every event is validated against its schema before publication.
Handling Distributed Transactions: The Saga Pattern
In event-driven systems, you can't use traditional database transactions across services. Instead, use the Saga pattern:
Choreography: Each service publishes events that trigger the next step. No central coordinator.
OrderService → OrderPlaced → PaymentService → PaymentProcessed → InventoryService → InventoryReserved → ShippingService
If any step fails, compensating events undo the previous steps:
InventoryService → InventoryReservationFailed → PaymentService → PaymentRefunded → OrderService → OrderCancelled
Orchestration: A central saga orchestrator directs each step and handles failures.
Choose choreography for simple flows (3-4 steps). Choose orchestration for complex flows (5+ steps, conditional logic, error handling).
When EDA Is Overkill
- Simple CRUD applications with straightforward request-response patterns
- Synchronous user interactions where the user expects immediate confirmation
- Small teams without the operational expertise to manage event infrastructure
- Low traffic systems where the complexity of EDA outweighs the scalability benefit
- Tightly coupled workflows where every step must succeed synchronously
When EDA Is the Right Choice
- Multiple consumers need to react to the same business event
- Independent scalability — different services have different load patterns
- Loose coupling is a priority (services evolve independently)
- Real-time processing — react to events as they happen
- Audit requirements — maintain a complete event history
- Integration — connecting systems from different teams or vendors
Event-driven architecture is a powerful tool when applied to the right problems. If you're designing an event-driven system or evaluating whether EDA is right for your architecture, let's talk.