Replication conflicts in CouchDB are not bugs; they’re a feature designed to preserve data integrity when multiple clients modify the same document concurrently.

Let’s see what this looks like in practice. Imagine you have a document representing a user’s profile:

{
  "_id": "user:alice",
  "_rev": "3-abc123def456",
  "name": "Alice",
  "email": "alice@example.com",
  "settings": {
    "theme": "dark"
  }
}

Now, Alice is editing her profile on her laptop, and simultaneously, an administrator is editing the same profile on a desktop. Both make changes and save them. When CouchDB replicates these changes, it can’t simply overwrite one with the other because both are valid, independent updates. Instead, it creates a conflict.

Here’s how CouchDB handles this, and how you can resolve it.

The Conflict Document

When a conflict occurs, CouchDB doesn’t pick a winner. Instead, it keeps both versions of the document. The document will have a special _conflicts field, listing the revision IDs of the conflicting versions. It also has a special _deleted field set to true for the winning revision (the one that arrived last chronologically, but not the one that was actually saved). This is a bit counter-intuitive: the document itself is marked as deleted, but the conflicting revisions are still there, accessible via the _conflicts array.

The document might look something like this after a conflict:

{
  "_id": "user:alice",
  "_rev": "4-ghi789jkl012", // This is the winning revision ID, but the document is marked as deleted.
  "_deleted": true,
  "_conflicts": {
    "revs": [
      "3-abc123def456",
      "5-mno345pqr678"
    ]
  },
  "name": "Alice", // Content from the winning revision
  "email": "alice@example.com", // Content from the winning revision
  "settings": {
    "theme": "dark"
  }
}

Here, 4-abc123def456 is the revision that was saved last from the perspective of the replication process that detected the conflict. However, the actual data associated with 3-abc123def456 and 5-mno345pqr678 are the two independent versions that need to be reconciled.

Resolving Conflicts: The Strategy

The core idea is to merge the conflicting revisions into a single, new, definitive revision. CouchDB itself won’t do this automatically; it requires your application logic to intervene.

The general workflow is:

  1. Detect Conflicts: Your application needs to query for documents that have the _conflicts field.
  2. Fetch Conflicting Revisions: For each conflicting document, retrieve all the revisions listed in _conflicts._conflicts.revs.
  3. Merge Data: Write application logic to intelligently merge the data from the conflicting revisions. This is the most complex part and depends entirely on your data model.
  4. Save the Merged Revision: Create a new document revision with the merged data. This new revision will become the "winner" and will effectively supersede all previous conflicting revisions.
  5. Clean Up Conflicts: Once the new merged revision is saved, CouchDB will automatically remove the _conflicts field from the document.

Practical Steps and Tools

1. Identifying Conflicts

You can query your database to find documents with conflicts using a _view:

Create a design document (e.g., _design/conflict_resolver):

{
  "_id": "_design/conflict_resolver",
  "views": {
    "conflicts": {
      "map": "function(doc) { if (doc._conflicts) { emit(doc._id, doc._conflicts); } }"
    }
  }
}

Then query it:

GET /your_database/_design/conflict_resolver/_view/conflicts

This will return a list of document IDs that have conflicts.

2. Fetching Conflicting Revisions

Once you have a conflicting document ID (e.g., user:alice) and its conflicting revision IDs (e.g., 3-abc123def456, 5-mno345pqr678), you need to fetch each of those specific revisions. You can do this by appending the revision ID to the document URL:

GET /your_database/user:alice?rev=3-abc123def456 GET /your_database/user:alice?rev=5-mno345pqr678

3. Merging Logic (The Application’s Job)

This is where your application shines. Let’s say Alice updated her settings.theme and the admin changed her name.

  • Revision 1 (Alice): _rev: "3-abc123def456", settings.theme: "dark"
  • Revision 2 (Admin): _rev: "5-mno345pqr678", name: "Alice Smith"

Your merging logic would look at these two versions and decide:

  • The name from revision 2 is the desired change.
  • The settings.theme from revision 1 is also desired.

You’d construct a new document that incorporates both:

{
  "_id": "user:alice",
  "name": "Alice Smith",
  "email": "alice@example.com", // Assuming this was common or from the winning revision
  "settings": {
    "theme": "dark"
  }
}

4. Saving the Merged Revision

You then PUT this new, merged document back to CouchDB. Crucially, you must provide one of the conflicting revision IDs in the rev parameter of your PUT request. CouchDB uses this to know which conflict you are resolving. It’s generally best practice to use the revision ID that came from the most recent successful save before the conflict was detected, or one that your application logic has determined is the "base" for the merge. In our example, let’s say we’ll use 5-mno345pqr678 as the base:

PUT /your_database/user:alice?rev=5-mno345pqr678

With the following JSON body:

{
  "name": "Alice Smith",
  "email": "alice@example.com",
  "settings": {
    "theme": "dark"
  }
}

CouchDB will then create a new revision (e.g., 6-xyz123abc456) that contains the merged data, and the _conflicts field will be removed.

5. Automation

For high-volume systems, you’ll want to automate this process. This can be done by:

  • Background Tasks: A separate process or worker that periodically scans for conflicts and resolves them.
  • Event Listeners (if using a framework): Some frameworks might offer hooks for handling document changes or replication events.
  • Client-Side Resolution: If your application has offline capabilities (like CouchDB’s PouchDB), conflict resolution often happens on the client before syncing.

The "Deleted" Document Trick

When CouchDB detects a conflict, it marks the winning revision (the one that arrived last in the replication stream) with _deleted: true. This might seem odd, but it’s how CouchDB signals that the document as a whole is in a conflicted state. The actual data for the conflicting versions is still available via their respective revision IDs. When you successfully PUT a merged revision, the _deleted flag is removed, and the _conflicts field is gone.

The next challenge you’ll face is managing the sheer volume of historical revisions that can accumulate, especially if conflicts aren’t resolved promptly.

Want structured learning?

Take the full Couchdb course →