Eventual consistency is not a compromise; it’s a feature that allows your system to achieve higher throughput and availability by deferring immediate global consistency for the sake of performance.
Let’s watch this happen. Imagine a typical e-commerce scenario. A customer places an order. This OrderPlaced event is published.
{
"eventType": "OrderPlaced",
"orderId": "ord-12345",
"customerId": "cust-abcde",
"items": [
{"productId": "prod-xyz", "quantity": 2}
],
"timestamp": "2023-10-27T10:00:00Z"
}
This event is consumed by multiple read models. One read model might be an OrderSummary projection that a customer sees on their "My Orders" page. Another might be an InventoryLedger projection that tracks stock levels.
Here’s a simplified view of the OrderSummary projection’s state:
{
"orderId": "ord-12345",
"customerId": "cust-abcde",
"status": "Pending",
"lastUpdated": "2023-10-27T10:00:05Z"
}
And the InventoryLedger might look like this:
{
"productId": "prod-xyz",
"availableStock": 98,
"lastUpdated": "2023-10-27T10:00:06Z"
}
The key is that these projections are updated asynchronously. The OrderPlaced event is processed by the OrderSummary projection handler, and then by the InventoryLedger handler. There’s a small window, perhaps milliseconds or seconds, where the OrderSummary is updated, but the InventoryLedger hasn’t caught up yet. This is eventual consistency.
The problem CQRS solves here is separating the command side (where orders are placed and state changes are initiated) from the query side (where read models are built and queried). In our example, OrderPlaced is a command that results in an event. The read models are the queries, built from these events.
The levers you control are primarily in how your event handlers are implemented and how your read models are persisted.
- Event Handlers: These are the services or functions that subscribe to events and update the read models. You can control their order of execution (if your message broker supports ordered partitions) and their reliability.
- Read Model Storage: This could be a SQL database, a NoSQL document store, or even an in-memory cache. The performance characteristics of this storage directly impact how quickly your read models become consistent.
- Message Broker: The choice of message broker (e.g., Kafka, RabbitMQ, Azure Service Bus) influences latency, durability, and ordering guarantees, all of which affect the observable consistency of your read models.
The most surprising thing about eventual consistency is that it often leads to better overall system performance and resilience than strict consistency, especially at scale. When you don’t need to wait for every single component to acknowledge a state change before proceeding, your system can handle more concurrent operations. Think of a busy restaurant: if the waiter had to wait for the chef, the sous chef, and the dishwasher to confirm a dish was ready before telling the customer, service would grind to a halt. Instead, the waiter gets an update from the chef, and the customer is informed, even if the dishwasher hasn’t finished yet.
The common pattern for managing eventual consistency in CQRS read models involves using a message broker to publish domain events. Each read model then subscribes to these events and updates its own data store. To ensure that a read model doesn’t miss events or process them out of order, you typically configure your message broker to provide ordering guarantees within a partition and use an idempotent processing mechanism within your event handlers. This means that if an event is delivered multiple times, it only has an effect once. For example, when updating a document in a NoSQL database, you might check if the document’s version or a timestamp already reflects a later change before applying the update.
The next challenge you’ll encounter is handling "thundering herd" scenarios where a large burst of events can overwhelm your read model processors.