The Anatomy of a Collision
When two devices independently modify the same record, the server rejects the second incoming delta, and the client must reconcile the competing changes. The SDK's resolution algorithm takes a single local change and a single remote change as input — each being an insert, update, or delete on one record — and produces a new snapshot plus rebased versions of both original operations.
The process begins with several trivial cases that are dispatched immediately:
- Changes on different tables commute.
- Changes on different records within the same table commute.
- A record deletion always wins over an update to that record.
- A local deletion paired with a remote deletion results in a no-op for the local side.
- Updates are merged field-by-field.
The rationale for deletion-over-update precedence is operational level: a record delete is a "higher-level" operation than a field update, so it should take priority in the merged result.
Field-Level Resolution Rules
When two updates target the same field, the SDK consults the field's declared resolution rule. Five rules are available, and the distinction between local and remote matters because the remote change may already be visible to other devices. The default rule favors the remote value, reflecting that it has already been observed by the wider system.
| Rule | rebased local | rebased remote | rebased end state |
|---|---|---|---|
| remote (default) | NOP (loses) | x:=2 (wins) | {x=2} |
| local | x:=1 (wins) | NOP (loses) | {x=1} |
| max | NOP (loses) | x:=2 (wins) | {x=2} |
| min | x:=1 (wins) | NOP (loses) | {x=1} |
| sum | x:=3 | x:=3 | {x=3} |
For the sum rule, the algorithm reinterprets both operations as additive deltas on the original value. If the field started at 0, a local assignment of 1 and a remote assignment of 2 become "add 1" and "add 2," which commute and combine into a single "add 3" operation. Since the server only accepts assignments, both rebased operations are rewritten as x:=3.
Merging Deltas with Multiple Changes
Real-world deltas contain many changes across various records and fields. To resolve such lists, the algorithm applies the basic single-change resolution pairwise, filling in a large grid of intermediate states. Starting from the original snapshot at the top-left and the final outcome at the bottom-right, each square in the grid represents merging one local change with one remote change.
The order of filling squares doesn't affect the result — row-by-row, column-by-column, or even zig-zag patterns all yield the same final state. This property holds for all interior nodes as well, provided the basic single-square method itself is confluent.
For a concrete example, consider local updates setting {name="James", age=9}, then {pal="Leo"}, then {age=10, sick=True}, conflicting with remote updates {name="Jim"} and {sick=False}. Resolving all six pairwise combinations produces a consistent final record.
Performance Considerations
A naive implementation that resolves every pair of N local and N remote changes approaches O(N²) complexity. However, many change sets trivially commute — different tables, different records, or different fields — allowing large grid sections to be skipped without computation. Sorting each delta's changes by table and record ID (while preserving order within a record) splits one large problem into several small ones.
In practice, conflicts are rare because most devices are online, and offline modifications typically happen on a single device. A worst-case pathological scenario is possible occasionally, but the algorithm's mathematical rigor means the SDK can be optimized internally without API changes.
Insertions and Field Deletions
Record inserts can conflict only when your app explicitly chooses the record ID. For example, using "firstname.lastname" as an ID means two offline devices inserting a "John Smith" contact will collide. The SDK resolves this by treating each insert as an empty-record creation followed by a field update; the empty inserts commute, and the resulting updates are resolved using the standard rules. If you're not comfortable with that outcome, let the SDK generate the ID, and the datastore simply keeps two separate records.
Field deletions currently take precedence over opposing field updates — a deletion always wins a field-level collision. An alternative model treats deletion as setting the value to null and runs it through the standard update-resolution rules. The SDK will likely adopt that behavior in the future, although the observable effect on the snapshot remains unchanged: the field is simply removed.
Collision Resolution Strategies in the Datastore API
When two clients modify the same record concurrently, the Dropbox Datastore API does not simply discard one change. Instead, it exposes the conflicting values to your application code through a set of DbxTable and DbxRecord methods designed for explicit resolution. This part of the series walks through the available strategies and how they map to the API's methods.
Understanding the Conflict Interface
Upon a sync cycle, the API identifies records that have been changed on both the local and remote ends. It then calls your conflict resolver, giving you access to both versions of the record: the local state (which your code has mutated) and the remote state (the canonical server-side version). The resolver must decide which value wins, or how to merge the two into a consistent result.
For each field in conflict, you can query the DbxRecord for both its local and remote values. The API offers getLocalValue() and getRemoteValue() methods that return the field's value as seen from each perspective. The key is that these values are only provided for fields where a genuine collision exists; fields that were not touched on either side are not included in this resolution payload.
Predefined Resolution Rules
The API ships with a convenience resolver that handles common cases without requiring custom code:
- Last writer wins: This rule compares the modification timestamps of the local and remote records and selects the newer one. It is the default behavior and is appropriate when data is immutable or when the latest change is inherently the most correct.
- Remote wins: For fields where the remote server version is authoritative — such as data managed by another system — the resolver simply overwrites the local value with the remote one.
- Local wins: The inverse approach, which prioritizes the client's unsynced changes. Use this when the local device is the primary source of truth and its edits should never be overridden.
These rules are applied at the field level, not the whole-record level. You could, for example, let the remote value win for a price field while keeping a local edit for a quantity field.
Custom Merging Logic
When the builtin rules do not suffice, you can implement a custom resolver. The DbxTable object lets you register a conflictResolver that receives each colliding record. Inside that resolver, you can inspect both the local and remote values for each field that appears in the conflict, apply application-specific logic, and then set the resulting value back onto the record before it is returned to the cloud.
A common example is a counter or an accumulator. Rather than choosing either value, you can compute the sum of the local and remote deltas and write that combined value. Similarly, for text fields, you could implement a list merge or concatenate values that are not mutually exclusive.
Once your resolver finishes, it must return a boolean indicating whether any fields were actually modified. This tells the API whether the final record needs to be written back to the server or if the resolution was a no-op.
Deletion Conflicts
One special case is when one side deletes a record while the other side edits it. Instead of discarding the edit, the API treats the deletion as a conflict. Your resolver then gets to decide: it can preserve the deletion, restore the remote edit, or merge the local values into a new record. The resolution API exposes the deleted state as a special flag on the record, so your custom code can distinguish between a normal edit-conflict and a delete-versus-edit situation.
Example Flow
The resolution cycle works as follows in practice:
- A sync starts, and both local and remote copies of a record are present with diverging changes.
- The API invokes the registered conflict resolver on the record.
- Your resolver calls
getLocalValue()andgetRemoteValue()on each colliding field and applies the desired rule or custom merge. - If changes were made, the resolver updates the record values, and the modified record is scheduled for upload.
- The cloud returns the final value, and all clients converge on the same state.
The key architectural takeaway is that the Datastore API does not keep extensive version history. Conflict resolution is a one-shot operation: the moment your resolver returns, the merged or chosen value becomes the single source of truth. There is no rollback or second round of negotiation. For many mobile apps, that's a price worth paying for offline-friendly sync with automatic collision detection.



