CouchDB’s conflict detection and resolution is a lot less about preventing conflicts and more about surviving them gracefully.
Let’s see this in action. Imagine two clients, Alice and Bob, both editing the same document my-doc.
Initial state of my-doc:
{
"_id": "my-doc",
"_rev": "1-abcde",
"content": "Initial version",
"edited_by": []
}
Alice reads my-doc (rev 1-abcde), adds her change:
{
"_id": "my-doc",
"_rev": "1-abcde",
"content": "Alice's change",
"edited_by": ["Alice"]
}
While Alice is offline, Bob reads my-doc (rev 1-abcde), adds his change:
{
"_id": "my-doc",
"_rev": "1-abcde",
"content": "Bob's change",
"edited_by": ["Bob"]
}
Now, Alice comes back online and tries to save her document. CouchDB sees that the document has been updated since Alice last read it. Instead of overwriting Bob’s changes (or Alice’s), CouchDB creates a conflict. The document now looks like this:
{
"_id": "my-doc",
"_rev": "2-fghij",
"content": "Alice's change",
"edited_by": ["Alice"],
"_conflicts": ["2-klmno"]
}
Notice the _conflicts array. This indicates that there’s another revision of this document with the same parent revision (1-abcde) that also diverged. Bob’s version is 2-klmno.
The core problem CouchDB solves is distributed data consistency without a central authority that can arbitrarily decide which change wins. When multiple clients can write to the same data independently, conflicts are inevitable if there’s no locking mechanism. CouchDB’s approach is "last write wins" at the replication level, but at the storage level, it preserves all divergent branches until a user-defined resolution occurs.
The key to understanding CouchDB’s conflict handling is the revision tree. Every time you update a document, CouchDB generates a new revision ID. If multiple updates happen concurrently on the same parent revision, they create branches in this revision tree. CouchDB doesn’t automatically merge these branches; it flags them as conflicts, requiring an application-level strategy to resolve them.
To see the conflicts programmatically, you’d query the document and inspect the _conflicts field. If _conflicts exists, you know there’s a conflict.
To resolve a conflict, you typically read all conflicting revisions, merge them in your application logic, and then write a new revision that incorporates the desired state. The _conflicts field will disappear once the conflict is resolved by writing a new, winning revision.
Let’s say we want to merge Alice’s and Bob’s changes. We’d first fetch both conflicting revisions:
- Alice’s version (winning revision):
{ "_id": "my-doc", "_rev": "2-fghij", "content": "Alice's change", "edited_by": ["Alice"], "_conflicts": ["2-klmno"] } - Bob’s version (conflicting revision):
To get this, you’d typically query with
?conflicts=trueand then fetch the specific conflicting revision ID. Let’s assume Bob’s version was:{ "_id": "my-doc", "_rev": "2-klmno", "content": "Bob's change", "edited_by": ["Bob"] }
Now, in your application, you’d manually craft a new document that merges these. For instance, you might decide to combine the edited_by arrays and keep Alice’s content, or perhaps a more sophisticated merge.
Let’s say we want to keep Alice’s content and append Bob to the edited_by list, then add a note about the merge:
{
"_id": "my-doc",
"_rev": "2-fghij", // Use the revision ID of the document you intend to "win"
"content": "Alice's change",
"edited_by": ["Alice", "Bob"],
"resolution_note": "Merged after conflict"
}
When you PUT this new document, CouchDB will atomically update the document, effectively overwriting the conflicting revisions with this new merged state. The _conflicts field will be gone, and the revision history will show a new branch point. The actual winning revision ID will be generated by CouchDB, e.g., 3-pqrst.
The most common way people think they should handle conflicts is by picking one revision and overwriting the other. This is incorrect for CouchDB. You must read all conflicting revisions, merge them in your application logic, and then PUT a new document that represents the merged state. The _rev in your PUT request should match the revision you intend to base your merge upon (e.g., Alice’s 2-fghij), and CouchDB will generate a new revision ID for the merged result.
A critical aspect of conflict resolution is knowing how to merge. CouchDB provides the mechanism, but your application logic must define the strategy. This could be as simple as "last writer wins" (by picking one of the conflicting revisions to base your merge on) or as complex as a three-way merge or custom business logic.
The next challenge you’ll encounter is designing a robust conflict resolution strategy that works for your specific application’s data model and user experience.