The most surprising truth about event stores is that they’re not just databases for events; they’re the time machine for your application’s state.

Let’s see one in action. Imagine we’re building a simple order management system. An order can be Created, Shipped, or Delivered. Instead of updating a single orders table, we’ll append each action as an event to an event store.

Here’s a simplified representation of events for a single order:

[
  {
    "eventId": "a1b2c3d4-e5f6-7890-1234-567890abcdef",
    "orderId": "ORD-123",
    "eventType": "OrderCreated",
    "timestamp": "2023-10-27T10:00:00Z",
    "payload": {
      "customerId": "CUST-456",
      "items": [
        {"productId": "PROD-789", "quantity": 2}
      ]
    }
  },
  {
    "eventId": "b2c3d4e5-f6a7-8901-2345-67890abcdef1",
    "orderId": "ORD-123",
    "eventType": "OrderShipped",
    "timestamp": "2023-10-27T11:30:00Z",
    "payload": {
      "trackingNumber": "TRK-XYZ789",
      "carrier": "FedEx"
    }
  },
  {
    "eventId": "c3d4e5f6-a7b8-9012-3456-7890abcdef12",
    "orderId": "ORD-123",
    "eventType": "OrderDelivered",
    "timestamp": "2023-10-28T09:15:00Z",
    "payload": {
      "deliveredTimestamp": "2023-10-28T09:10:00Z"
    }
  }
]

The core problem event sourcing solves is that traditional databases struggle with historical accuracy and the "why" behind state changes. When you query an orders table, you see the current state. You don’t know how it got there, what decisions were made, or what intermediate states existed. Event sourcing makes every past state first-class data.

An event store is fundamentally an append-only log. You can only add new events; you cannot modify or delete existing ones. This immutability is key. To get the current state of an entity (like an order), you "replay" all events associated with that entity from its beginning.

Consider the ORD-123 example. To get its current state, an application would:

  1. Fetch all events for ORD-123.
  2. Start with an empty order object.
  3. Apply OrderCreated event: the order now has customerId: "CUST-456" and items.
  4. Apply OrderShipped event: the order now has trackingNumber: "TRK-XYZ789" and carrier: "FedEx".
  5. Apply OrderDelivered event: the order now has deliveredTimestamp: "2023-10-28T09:10:00Z".

The final state is the order as delivered. But crucially, we also have the state of the order when it was created, and when it was shipped, readily available.

The exact levers you control involve how you define your events and how you handle their serialization and deserialization.

  • Event Naming: Use clear, imperative verbs like OrderCreated, ItemAdded, PaymentReceived. This makes the log readable and the state transitions obvious.
  • Payload Structure: Design your event payloads to contain all necessary data to reconstruct the state change. If a field changes, include the new value. If an item is added to a list, include the item details.
  • Serialization: Choose a robust format like JSON or Protobuf. Protobuf is often preferred for performance and schema evolution.
  • Stream Identification: Events are typically grouped into "streams" (e.g., all events for ORD-123 form a single stream). The event store manages these streams.

Choosing an event store involves considering persistence, read performance, and query capabilities.

  1. Dedicated Event Stores:

    • EventStoreDB: A popular, open-source, production-ready event store designed from the ground up for this purpose. It offers features like projections, streams, and robust APIs.
    • NEventStore: A .NET-specific library that provides an abstraction over various storage mechanisms (SQL Server, RavenDB, etc.).
  2. General-Purpose Databases with Event Sourcing Patterns:

    • PostgreSQL: Can be used by creating an events table with columns like stream_id, event_type, sequence_number, payload (JSONB), and timestamp. You’d enforce immutability and stream atomicity with application logic and constraints.
    • Kafka: While primarily a message queue, Kafka’s immutable, ordered logs can serve as an event store. You’d need to manage stream concepts and state reconstruction on the consumer side.

The most common mistake is treating an event store like a regular database and attempting to query it directly for current state across many entities. The power of an event store lies in its append-only nature and the ability to replay events to reconstruct any past state, not in its ability to perform complex ad-hoc queries on the log itself. For querying current state efficiently, you’d typically build read models (projections) from the event stream.

To effectively query the current state of many orders, you’ll need to implement projections, which are materialized views built by subscribing to the event stream and updating a separate read-optimized database.

Want structured learning?

Take the full Event-driven course →