What is Event-Driven Architecture?
Event-Driven Architecture (EDA) is a design pattern where communication between systems occurs through events (notifications of occurrences). It reduces dependencies between components and enables building scalable and flexible systems.
What is an event: A message representing a meaningful state change within the system. Examples include “User registered,” “Order completed,” and “Inventory decreased.”
Differences from Traditional Request-Driven Architecture
Request-Driven (Synchronous)
flowchart LR
Order["Order Service"] --> Inventory["Inventory Service"]
Inventory --> Payment["Payment Service"]
Payment --> Notification["Notification Service"]
- Services are tightly coupled
- One service failure affects the entire system
- Processing time is the sum of all services
Event-Driven (Asynchronous)
flowchart TB
Order["Order Service"] --> Event["Order Completed Event"]
Event --> Inventory["Inventory Service<br/>(processes independently)"]
Event --> Payment["Payment Service<br/>(processes independently)"]
Event --> Notification["Notification Service<br/>(processes independently)"]
- Services are loosely coupled
- Failures are localized
- Parallel processing is possible
Types of Events
Domain Events
Represent occurrences in the business domain.
// Domain event example
{
"eventType": "OrderPlaced",
"eventId": "evt_123456",
"timestamp": "2024-01-15T10:30:00Z",
"payload": {
"orderId": "ord_789",
"customerId": "cust_456",
"items": [...],
"totalAmount": 5000
}
}
Integration Events
Events shared between different services.
Notification Events
Events that only notify of state changes without containing detailed data.
// Notification events (Fat vs Thin)
// Thin Event - details must be fetched separately
{ "eventType": "UserUpdated", "userId": "123" }
// Fat Event - contains all necessary information
{ "eventType": "UserUpdated", "userId": "123", "name": "Alice", "email": "..." }
Event Sourcing
A pattern where state is stored as a history of events.
Traditional CRUD
Store only current state:
| Table | id | status |
|---|---|---|
| orders | 1 | ”shipped” |
Event Sourcing
Store all events:
| events | timestamp |
|---|---|
| 1. OrderCreated | 2024-01-01 |
| 2. PaymentReceived | 2024-01-02 |
| 3. OrderShipped | 2024-01-03 |
↓ Replay → Current state: status = “shipped”
Benefits
- Complete audit trail: Can track all change history
- Time travel: Can reconstruct state at any point in time
- Event replay: Can rebuild state after bug fixes
Drawbacks
- Increased complexity
- Event schema evolution is challenging
- Read performance requires careful consideration
CQRS (Command Query Responsibility Segregation)
A pattern that separates read (Query) and write (Command) models.
flowchart TB
Command["Command<br/>(Write Model)"] -->|Publish Events| EventStore["Event Store"]
EventStore -->|Project| Query["Query<br/>(Read Model)"]
Write Model
// Command handler
async function handlePlaceOrder(command) {
const order = new Order(command.orderId);
order.addItems(command.items);
order.place();
await eventStore.save(order.getUncommittedEvents());
}
Read Model
// View optimized for reading
const orderSummary = {
orderId: "123",
customerName: "Alice", // Customer info already joined
itemCount: 3,
totalAmount: 5000,
status: "shipped"
};
Implementation Patterns
Saga Pattern
Achieves transactions spanning multiple services through a chain of events.
flowchart LR
subgraph Success["Success Path"]
S1["Create Order"] --> S2["Reserve Inventory"]
S2 --> S3["Process Payment"]
S3 --> S4["Confirm Order"]
end
subgraph Failure["Failure Path (Compensation)"]
F1["PaymentFailed"] --> F2["Release Inventory"]
F2 --> F3["Cancel Order"]
end
Outbox Pattern
A pattern that ensures both database update and event publishing succeed.
| Step | Phase | Actions |
|---|---|---|
| 1 | Within a transaction | Update business data, Insert event into outbox table |
| 2 | In a separate process | Poll outbox table, Publish events to message queue, Mark as published |
Considerations
Eventual Consistency
| Service | State | Status |
|---|---|---|
| Order Service | Order status = “completed” | ✓ Updated |
| Inventory Service | Event not yet processed | Inconsistency period |
| Inventory Service | Inventory decreased | Consistency restored |
Event Ordering
| Correct order | If order is disrupted |
|---|---|
| 1. OrderCreated | 1. OrderShipped (?) |
| 2. OrderUpdated | 2. OrderCreated |
| 3. OrderShipped | → State may be corrupted |
Idempotency
Design so that the same event delivered multiple times produces the same result.
async function handleInventoryReserved(event) {
// Check for duplicates using idempotency key
const processed = await db.processedEvents.findById(event.eventId);
if (processed) return;
await db.inventory.reserve(event.payload);
await db.processedEvents.insert({ eventId: event.eventId });
}
Summary
Event-Driven Architecture is a powerful pattern for designing complex systems with loose coupling and flexibility. Combined with Event Sourcing and CQRS, it can improve auditability and scalability. However, you need to address challenges specific to distributed systems, such as eventual consistency and event ordering.
← Back to list