CouchDB doesn’t have transactions in the relational database sense, but you can achieve multi-document consistency through clever application design and CouchDB’s built-in features.

Let’s see this in action. Imagine we’re building a simple inventory system where an order document needs to deduct stock from a product document. A naive approach might be:

  1. Fetch the product document.
  2. Update its stock count.
  3. Save the product document.
  4. Create the order document.

This works fine until something goes wrong between steps 3 and 4. What if the CouchDB server crashes, or the network connection drops? The product stock is updated, but the order is never created, leaving your inventory in an inconsistent state.

CouchDB’s atomic update mechanism for single documents is key here. You can update a document and its revision atomically. For multi-document operations, we leverage this by making the creation of a new document (like an order) dependent on the successful update of another document (like a product).

The core pattern involves using a design document and a _update handler. This handler is a JavaScript function that runs on the CouchDB server. It receives the document to be updated (or a new document if it doesn’t exist) and a request body. It then performs logic and returns the new document state.

Here’s a simplified example of a design document with an _update handler for processing an order and deducting stock:

// _design/inventory

{
  "_id": "_design/inventory",
  "language": "javascript",
  "updates": {
    "process_order": "function(doc, req) { \n  var order_data = JSON.parse(req.payload); \n  \n  // Check if product exists and has enough stock\n  if (!doc || doc.stock < order_data.quantity) { \n    return [null, {\"error\": \"Insufficient stock or product not found\"}]; \n  } \n  \n  // Deduct stock\n  doc.stock -= order_data.quantity; \n  \n  // Return the updated product doc and the order doc (which is created separately but conceptually linked)\n  // In a real scenario, you might create the order doc here if CouchDB supported multi-doc transactions from update handlers.\n  // For now, we'll just update the product and the application will create the order.\n  return [doc, {\"message\": \"Stock updated successfully\"}]; \n}"
  }
}

In this example, the process_order update handler is designed to be called on the product document. It takes the order quantity from the request payload. It checks if the product document exists and has sufficient stock. If not, it returns an error. If sufficient, it deducts the quantity from stock and returns the updated product document.

The critical part is that the _update handler runs atomically on the server. This means the stock deduction happens as a single, indivisible operation.

Now, how do we tie this to creating the order? The application logic would look like this:

  1. Start a batch operation: In your application code, you’d begin a sequence of operations that you want to treat as a single unit.
  2. Process the product update:
    • Send a POST request to /_design/inventory/_update/process_order/product_id?new_edits=false with a JSON payload like {"quantity": 5}.
    • The new_edits=false parameter is important; it tells CouchDB to generate a new revision for the document being updated.
  3. Handle the response:
    • If the process_order handler succeeds (returns 200 OK), you get the updated product document back. This is your signal to proceed.
    • If it fails (e.g., insufficient stock, returns 400 Bad Request or 404 Not Found), you stop and report the error. The product document remains unchanged.
  4. Create the order document: Only if the product update was successful, create the order document. This order document would reference the product_id and the quantity that was ordered.
// Example application logic (conceptual, e.g., Node.js with nano client)

async function placeOrder(productId, quantity) {
  const productDoc = await db.get(productId); // Get current product revision for the order doc
  const orderPayload = {
    _id: `order_${Date.now()}`, // Generate a unique ID
    productId: productId,
    quantity: quantity,
    status: 'pending',
    productRev: productDoc._rev // Store the revision of the product at the time of order
  };

  try {
    // Step 1: Update product stock using the _update handler
    const productUpdateResponse = await db.insert({
      _id: productId,
      _rev: productDoc._rev, // Must provide the current revision to update
      stock: productDoc.stock - quantity,
      // ... other product fields
    }, `_design/inventory/process_order/${productId}?new_edits=false`); // Using _update handler

    // In a real scenario, you would use the _update handler directly,
    // but for demonstration, we're showing the application-level check.
    // The _update handler is more for complex server-side logic.
    // Let's assume we're manually doing the check and update here for simplicity
    // and then creating the order.

    // A more robust approach uses the _update handler like this:
    // const productUpdateResult = await db.atomicUpdate(
    //   '_design/inventory/process_order',
    //   productId,
    //   { payload: JSON.stringify({ quantity: quantity }) }
    // );

    // If the product update in _update handler fails, it will throw an error.
    // Let's simulate the check here:
    if (productDoc.stock < quantity) {
      throw new Error('Insufficient stock');
    }

    // If stock is sufficient, update the product document.
    // In a real _update handler, this would be the server-side logic.
    // Here, we'll perform the update and then create the order.
    const updatedProduct = {
      ...productDoc,
      stock: productDoc.stock - quantity
    };
    await db.put(updatedProduct); // Update the product document

    // Step 2: Create the order document *only if product update succeeded*
    const orderCreationResult = await db.put(orderPayload);
    console.log('Order placed successfully:', orderCreationResult);
    return orderCreationResult;

  } catch (error) {
    console.error('Failed to place order:', error.message);
    // If product update failed, the order is not created.
    // If order creation failed *after* product update, you have an inconsistency.
    // This is where replication and conflict resolution come in.
    throw error;
  }
}

The trick to avoiding inconsistency when the order creation fails after the product update succeeds is to make the order document itself contain information that allows for a rollback or correction. A common pattern is to include the _rev of the product document at the time of the order in the order document.

If you later need to reconcile, you can query for orders and check if the product document’s current revision matches the productRev stored in the order. If not, it indicates a potential issue or that the order has already been processed against a different product revision. You can then use a View or a _changes feed to detect and handle these discrepancies.

Another powerful pattern is using CouchDB’s _changes feed with a client-side process that listens for changes. When a product document is updated by the _update handler, it emits a change. Your client-side process can then react to this change and create the corresponding order document. If the client-side process crashes between the product update and order creation, it can resume from the last processed change in the _changes feed, ensuring that no operations are missed.

The _changes feed is your reliable log. You can process events from the _changes feed, perform your multi-document logic, and if an operation fails mid-way, the _changes feed will still contain the event, allowing your process to retry.

The one thing most people don’t know is that you can use the _update handler to create a new document as well, not just update an existing one. By checking if doc is null in the handler, you can write logic that, for instance, creates a "transaction log" document that encapsulates multiple conceptual operations. This document can then be used as the single atomic point of truth. The application then reads this log document to determine what actions to take.

The next concept to explore is using CouchDB’s replication to distribute this consistency logic across multiple servers or for offline-first applications.

Want structured learning?

Take the full Couchdb course →