CouchDB’s eventual consistency is its greatest strength when acting as an event store, not a bug.

Let’s see it in action. Imagine we have a simple OrderService that handles creating new orders. Instead of directly updating an order’s state in a traditional database, we’ll append an OrderCreated event to our CouchDB event store.

// Example: Appending an event to CouchDB
async function createOrder(orderData) {
  const orderId = generateOrderId(); // UUID or similar
  const event = {
    _id: `order-${orderId}`, // Document ID for the order
    type: 'OrderCreated',
    timestamp: new Date().toISOString(),
    payload: {
      ...orderData,
      orderId: orderId
    }
  };

  try {
    await db.insert(event); // 'db' is your CouchDB database object
    console.log(`Order created event appended for order ${orderId}`);
    return orderId;
  } catch (error) {
    console.error('Failed to append event:', error);
    throw error;
  }
}

When an OrderCreated event is appended, CouchDB doesn’t guarantee immediate global consistency. Instead, it ensures that the event is durably stored. Other services, like an InventoryService or a NotificationService, can then subscribe to changes in the event store (using _changes feed) and react to the OrderCreated event independently.

The core problem event sourcing solves is gaining a complete, auditable history of how an application’s state evolved. Instead of just knowing the current state of an order, you know every single action that led to it: it was created, an item was added, the address was changed, it was paid for, it was shipped, etc. Each of these actions is an "event."

CouchDB fits into this by providing a robust, distributed, and append-only log for these events. Each event is a document. The document’s _id often incorporates the entity ID and a sequence number (e.g., order-123-1, order-123-2) to maintain order for a specific entity, or simply the entity ID if you’re relying on CouchDB’s sequential _changes feed for global ordering.

Here’s the mental model:

  1. Events are First-Class Citizens: Every significant action in your system becomes an immutable event record stored in CouchDB.
  2. Append-Only Log: You only ever add new event documents. You don’t update or delete them, preserving the audit trail.
  3. Replayability: To reconstruct the current state of an entity (e.g., an order), you read all events for that entity from CouchDB and apply them in sequence. This is called "replaying the events."
  4. Decoupled Consumers: Other services listen to CouchDB’s _changes feed. When a new event is appended, these services are notified and can process it asynchronously. This is where eventual consistency shines – services don’t need to wait for each other.
  5. Projections/Read Models: For efficient querying, you build "read models" or "projections." These are separate CouchDB documents (or even separate databases) that are updated by services reacting to events. For example, a OrderSummary view might be updated by a service that listens to all order-related events.

Consider how you’d query for all events related to a specific order. You’d use a _view or _find query:

// Example: Retrieving all events for a specific order ID
async function getOrderEvents(orderId) {
  const query = {
    selector: {
      _id: { $regex: `^order-${orderId}-.*` } // Assuming _id format like order-123-1, order-123-2
    },
    sort: [{ _id: 'asc' }] // Ensure chronological order
  };

  try {
    const result = await db.find(query);
    return result.docs;
  } catch (error) {
    console.error('Failed to retrieve order events:', error);
    throw error;
  }
}

The magic of CouchDB’s _changes feed is that it provides a globally ordered, monotonically increasing stream of all document changes. While individual document updates might be eventually consistent across replicas, the _changes feed itself is a reliable source of truth for what changed and when in terms of the sequence of operations. This feed is what your other services would poll or use with long-polling to react to new events.

What most people miss is that the _id of an event document doesn’t have to include a sequence number if you’re solely relying on the _changes feed for ordering. If you create documents like order-123 and simply append new versions (with _rev updates), CouchDB’s _changes feed will still list them in the order they were committed to the database on the master. This simplifies event ID generation, but you lose direct chronological ordering within a single entity’s event history if you don’t use a sequence number in the _id or a separate timestamp field for sorting within a query. For strict event sourcing, a composite _id like entityId-sequenceNumber or using the _changes feed with a since parameter is crucial for replayability.

The next step is managing the state projections and dealing with the potential for a large number of event documents.

Want structured learning?

Take the full Couchdb course →