> ## Documentation Index
> Fetch the complete documentation index at: https://docs.ditto.live/llms.txt
> Use this file to discover all available pages before exploring further.

# Conflict Resolution Patterns

> How Ditto's CRDT merge semantics give you deterministic, automatic conflict resolution through data model design.

In traditional distributed systems, handling concurrent edits from multiple devices requires custom merge logic — code that's hard to write, hard to test, and hard to get right. Ditto eliminates this entirely. Its built-in CRDT (Conflict-free Replicated Data Type) semantics give every field deterministic merge rules, so concurrent edits resolve automatically and consistently across all devices with zero custom code.

The key is choosing the right data structures. This guide shows you how to model your data so that Ditto's merge rules produce the correct business outcome — every time, on every device, even when devices are offline.

## Custom Conflict Resolution: Read-Time Derivation

Ditto's built-in CRDT merge rules handle most concurrent edit scenarios automatically. But when your domain has **custom conflict resolution needs** — for example, a status field that should only move forward through a defined progression — you need a strategy that goes beyond default merge behavior.

Some distributed databases solve this with a **write-time conflict resolver**: a callback that examines incoming writes, picks a winner, and discards the loser. The rejected write is gone forever. This means the database is making business logic decisions, and you only get one shot to get the merge right.

With Ditto, you can take a different approach: **accept every write** using add-wins maps, then **derive** the correct business state at read time. Your application examines the complete history of writes and produces the same deterministic answer on every device — using whatever business rules fit your domain.

This read-time derivation approach has several advantages over write-time rejection:

* **No data loss.** Every write from every device is preserved. A write-time resolver can only see one side of a conflict at a time, and rejected writes are gone forever.
* **Full auditability.** The complete event history is available for debugging, compliance, and analytics.
* **Testable logic.** The conflict resolution rules live in your application's read path — not in an opaque database callback — where they're testable, versionable, and under your direct control.

The [audit log pattern](#audit-logs-preserving-every-state-transition) below is the primary tool for implementing read-time derivation.

<CardGroup>
  <Card title="Document Model" icon="circle-nodes" iconType="solid" href="/key-concepts/document-model">
    How Ditto documents work internally as CRDTs with automatic conflict resolution.
  </Card>

  <Card title="Data Modeling Tips" icon="cube" iconType="solid" href="/best-practices/data-modeling">
    Guidance on document size, embedded vs flat models, and structuring collections.
  </Card>

  <Card title="Schema Versioning" icon="code-branch" iconType="solid" href="/best-practices/schema-versioning">
    Strategies for evolving your schema in a distributed environment.
  </Card>

  <Card title="DQL INSERT" icon="file-import" iconType="solid" href="/dql/insert">
    Full reference for INSERT statements including conflict handling clauses.
  </Card>
</CardGroup>

## Maps: Automatic Merge for Collections

Ditto maps are the go-to structure for any collection of items that multiple devices might edit. Maps use **add-wins merge** — when two devices concurrently add entries with different keys, Ditto automatically preserves both. Each scalar value within a map entry gets its own Last Writer Wins (LWW) register, so concurrent edits to different fields within the same entry also merge cleanly.

This means you can have two devices adding items, updating different fields, or working on different entries — and everything merges correctly without any conflict resolution code.

### When to use maps vs arrays

Arrays are simpler but use whole-value LWW — the entire array is treated as one unit. This is perfectly fine when only one device writes to the array. For multi-writer collections, maps give you automatic, fine-grained merge.

| **Maps** (multi-writer safe)                    | **Arrays** (single-writer)                              |
| ----------------------------------------------- | ------------------------------------------------------- |
| Multiple devices may add, edit, or remove items | Only one device writes; others read                     |
| Items have a natural or synthetic unique ID     | Order matters and the list is never edited concurrently |
| You want to index into individual items         | Simple list of scalars replaced wholesale               |

### Example: tasks in a project

<Tabs>
  <Tab title="Concept">
    Model tasks as a map keyed by task ID, and Ditto handles the rest:

    ```json theme={null}
    {
      "_id": "123",
      "tasks": {
        "t1": { "title": "Design mockups", "status": "done" },
        "t2": { "title": "Write API", "status": "in_progress" }
      }
    }
    ```

    Two devices can add different tasks, or edit different fields on the same task, and all changes merge automatically after sync.
  </Tab>

  <Tab title="DQL">
    ```sql theme={null}
    -- :doc is a bound parameter containing the document as a native object from your SDK
    INSERT INTO projects DOCUMENTS (:doc)
    ON ID CONFLICT DO UPDATE_LOCAL_DIFF

    -- Add a new task -- merges automatically with any concurrent additions
    UPDATE projects
    SET tasks.t3 = { 'title': 'Deploy to staging', 'status': 'todo' }
    WHERE _id = '123'

    -- Update a single field within a task
    UPDATE projects
    SET tasks.t2.status = 'done'
    WHERE _id = '123'

    -- Query a specific task's status
    SELECT tasks.t2.status
    FROM projects
    WHERE _id = '123'
    ```
  </Tab>
</Tabs>

<Info>
  `UPDATE_LOCAL_DIFF` is available in SDK 4.12 and later. See [Smart Upserts with UPDATE\_LOCAL\_DIFF](#smart-upserts-with-update_local_diff) for details on how it works.
</Info>

### Choosing map keys

Use a **natural key** when one exists — an ID already unique within the collection (e.g., a payment ID, a SKU, a username). When no natural key exists, generate a **synthetic UUID** and use that as the key.

### Removing map entries

Because add-wins ensures concurrent additions are never lost, removing a map entry uses an explicit `UNSET` rather than simply omitting the key from a write. This keeps the merge semantics clean and predictable — Ditto always knows the difference between "unchanged" and "intentionally removed."

<Tabs>
  <Tab title="Concept">
    Omitting a key from a document write means "no change" — the entry stays. To remove it, use UNSET:

    ```text theme={null}
    Writing the document WITHOUT t3 does NOT delete it:
    { "tasks": { "t1": { ... }, "t2": { ... } } }

    Ditto sees t3 as unchanged, not deleted. It stays in the document.
    ```
  </Tab>

  <Tab title="DQL">
    ```sql theme={null}
    -- Explicitly remove a task
    UPDATE projects
    UNSET tasks.t3
    WHERE _id = '123'
    ```
  </Tab>
</Tabs>

## Audit Logs: Preserving Every State Transition

For fields that track status or progress — where transitions happen over time and every step matters — Ditto's add-wins maps give you a powerful pattern: the **audit log**.

Instead of a single status field where one device's update could overwrite another's, model status as a **timestamp-keyed map**. Each device appends its transitions, and Ditto's add-wins merge preserves all of them. No transitions are ever lost, and every device sees the same complete history.

The real power is in how you **derive** the current value. Because you have the full log, your application chooses the derivation logic that fits your business rules — you're not locked into "last write wins."

<Tabs>
  <Tab title="Concept">
    A timestamp-keyed audit log automatically preserves the full history:

    ```json theme={null}
    {
      "status_log": {
        "2025-06-01T10:00:00.000Z": "created",
        "2025-06-01T10:05:12.437Z": "confirmed",
        "2025-06-01T10:30:45.891Z": "shipped"
      }
    }
    ```

    Use millisecond-precision ISO 8601 timestamps as map keys to ensure each transition gets its own unique key.

    Ditto's add-wins merge guarantees that every device's transitions are preserved — even if two devices write concurrently while offline.
  </Tab>

  <Tab title="DQL">
    ```sql theme={null}
    -- Append a status transition -- merges with any concurrent transitions
    UPDATE orders
    SET status_log.`2025-06-01T10:30:45.891Z` = 'shipped'
    WHERE _id = '456'

    -- Query the full status history
    SELECT status_log
    FROM orders
    WHERE _id = '456'
    ```
  </Tab>
</Tabs>

### Choosing a derivation strategy

With a single LWW register, the merge strategy is fixed — the last write wins, period. With an audit log, **your application controls the derivation logic**. Common strategies include:

* **Latest timestamp:** Sort the log keys and read the last entry. Good for simple linear workflows.
* **Most advanced state:** Define a progression order (e.g., `created` \< `confirmed` \< `shipped` \< `delivered`) and pick the entry that has progressed the furthest, regardless of timestamp. This way, if one device writes `shipped` and another concurrently writes `confirmed`, the result is `shipped` — not whichever happened to have a later clock.
* **Earliest timestamp:** Take the first occurrence of a particular state. Useful for "when did this first happen?" queries like first acknowledgment or first error.
* **Custom business logic:** Combine multiple entries to compute a derived value. For example, a workflow might consider an order "fulfilled" only after both `shipped` and `delivered` entries exist.

The audit log stores the facts. Your derivation function interprets them. This separation means you can change how you interpret the log without changing the data — and every device deterministically arrives at the same answer from the same set of entries.

**Example: stale writes don't cause regressions.** Suppose a car's service status follows a known progression: `checked_in` → `inspecting` → `repair_in_progress` → `completed`. One device writes `repair_in_progress` while a stale device — still catching up after being offline — concurrently writes `inspecting`. The audit log preserves both entries. At read time, your derivation function walks the log and picks the most advanced valid state: `repair_in_progress` wins over `inspecting`, regardless of which write arrived first. The stale event is retained for auditability but does not affect the derived status.

### Great use cases for audit logs

This pattern works well for any field that moves through **states over time** — where the full history matters and the "current" value is a function of all transitions:

* Order or request lifecycle (created, confirmed, shipped, delivered)
* Approval workflows (submitted, reviewed, approved, rejected)
* Device state tracking (online, syncing, offline, error)
* Edit history and activity logs
* Progress tracking where "most advanced" beats "most recent"

<Info>
  You can also flip the pattern: use the **status value as the key** and the **timestamp as the value**. This guarantees unique keys (each status appears at most once) and makes it easy to check whether a specific transition has occurred:

  ```json theme={null}
  {
    "status_log": {
      "created": "2025-06-01T10:00:00.000Z",
      "confirmed": "2025-06-01T10:05:12.437Z",
      "shipped": "2025-06-01T10:30:45.891Z"
    }
  }
  ```

  Note that because each status appears at most once as a key, the timestamp value uses LWW — if a status is re-entered (e.g., `inspecting` → `repair_in_progress` → `inspecting` again), only the latest timestamp for that status is kept. This makes the status-keyed variant best suited for linear progressions where each state is entered once. Choose timestamp-keyed for full chronological history, or status-keyed for "did this state happen and when."
</Info>

### When a simple register is enough

Not every field needs an audit log. Ditto's default LWW register works well when:

* Only one device writes the field at a time (e.g., a user's display name)
* "Last write wins" is the desired behavior (e.g., a `last_modified` timestamp)
* The field is set once and never updated

## Smart Upserts with UPDATE\_LOCAL\_DIFF

When your application writes a full document to Ditto, `ON ID CONFLICT DO UPDATE_LOCAL_DIFF` ensures that only the fields you actually changed get new LWW timestamps. Ditto automatically diffs the incoming document against what's stored and skips unchanged fields — so concurrent changes from other devices to different fields are preserved.

<Tabs>
  <Tab title="Concept">
    ```
    Device A reads: { "title": "Draft", "assignee": "Alice" }
    Device B reads: { "title": "Draft", "assignee": "Alice" }

    Device A writes: { "title": "Final", "assignee": "Alice" }  -- changed title
    Device B writes: { "title": "Draft", "assignee": "Bob" }    -- changed assignee

    Result: { "title": "Final", "assignee": "Bob" }  -- both changes preserved
    ```

    Ditto recognizes that Device A only changed `title` and Device B only changed `assignee`, so both edits survive.
  </Tab>

  <Tab title="DQL">
    ```sql theme={null}
    -- :doc is a bound parameter from the SDK containing the full document
    -- Ditto diffs automatically -- only changed fields are updated
    INSERT INTO projects DOCUMENTS (:doc)
    ON ID CONFLICT DO UPDATE_LOCAL_DIFF
    ```
  </Tab>
</Tabs>

This is especially valuable when your app writes full documents from local state (a common pattern when syncing from an existing data layer) rather than surgical field-level updates.

<Info>
  `UPDATE_LOCAL_DIFF` is available in SDK 4.12 and later. Older SDKs should use field-level `UPDATE ... SET` statements instead of full-document upserts.
</Info>

## Denormalized Documents: One Document, Atomic Sync

Ditto is designed around **denormalized, self-contained documents** where all related data lives together. This gives you several advantages:

* **Atomic writes.** A single document write is atomic — no partial states, no multi-document transaction coordination.
* **Complete sync.** The full document syncs as a unit. There's no risk of a parent arriving without its children.
* **Simple reads.** All related data in a single fetch, no joins required.

Because Ditto's maps provide fine-grained merge at every level of nesting, you get the best of both worlds: a single document for simplicity, with independent merge behavior for each sub-entity inside it.

<Tabs>
  <Tab title="Concept">
    Keep related data together — each sub-entity merges independently via its own map key:

    ```json theme={null}
    {
      "_id": { "location": "42", "orderId": "789" },
      "cart": {
        "item-1": { "name": "Widget", "qty": 2, "price": { "amount": 999, "currency": "USD" } },
        "item-2": { "name": "Gadget", "qty": 1, "price": { "amount": 1499, "currency": "USD" } }
      },
      "payments": {
        "pay-1": { "method": "card", "amount": { "amount": 2498, "currency": "USD" } }
      },
      "status_log": {
        "2025-06-01T10:00:00.000Z": "created"
      }
    }
    ```

    One device can add a cart item while another adds a payment — Ditto merges both automatically.
  </Tab>

  <Tab title="DQL">
    ```sql theme={null}
    -- Single insert creates the full document
    INSERT INTO orders DOCUMENTS (:doc)
    ON ID CONFLICT DO UPDATE_LOCAL_DIFF

    -- A different device adds a payment -- automatically merges with existing data
    UPDATE orders
    SET payments.`pay-2` = {
      'method': 'cash',
      'amount': { 'amount': 500, 'currency': 'USD' }
    }
    WHERE _id.orderId = '789'
    ```
  </Tab>
</Tabs>

### When to use separate collections

Use separate collections when sub-entities are **independently accessed or owned by different permission scopes** — not simply because the document is getting large. For document size guidance, see [Data Modeling Tips](/best-practices/data-modeling).

## Structuring `_id` for Access Control

Ditto's permissions system uses **queries on `_id` subfields** to control which documents a device can read and write. By structuring your `_id` as a composite object with hierarchical fields, you get flexible, declarative access control built right into your data model.

<Tabs>
  <Tab title="Concept">
    ```json theme={null}
    {
      "_id": {
        "region": "1",
        "location": "42",
        "orderId": "789"
      }
    }
    ```

    Permissions can grant access at any level of the hierarchy. **Region-wide:** a device sees all documents where `_id.region = '1'`. **Location-scoped:** a device sees only its own location's documents where `_id.location = '42'`.
  </Tab>

  <Tab title="DQL">
    ```sql theme={null}
    -- Insert with a hierarchical _id
    INSERT INTO orders DOCUMENTS (:doc)
    ON ID CONFLICT DO UPDATE_LOCAL_DIFF

    -- Query all orders for a specific location
    SELECT *
    FROM orders
    WHERE _id.location = '42'
    ```
  </Tab>
</Tabs>

For full details on configuring permissions rules, see [Authentication and Authorization](/key-concepts/authentication-and-authorization).

## Indexing: Maps Unlock Deeper Queries

Ditto supports single-field indexes on scalar values, including nested fields via dot notation. Maps give you a significant indexing advantage: scalar fields within map entries can be indexed via their full path, while array elements cannot be indexed at all.

This means that converting a collection from an array to a map not only gives you better merge behavior — it also makes the contents queryable and indexable.

```sql theme={null}
-- Dot-notation paths through maps are indexable
CREATE INDEX IF NOT EXISTS idx_location
ON orders (_id.location)
```

For more on indexing capabilities, see [DQL Indexing](/dql/indexing).

## Quick Reference

| Data pattern                                               | Recommended structure                                                            | How Ditto merges it                                                                    |
| ---------------------------------------------------------- | -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
| **Collection of entities** (items, payments, participants) | Map keyed by ID                                                                  | Add-wins: concurrent additions are all preserved                                       |
| **Status / state machine**                                 | Timestamp-keyed map (audit log)                                                  | Add-wins: every transition preserved; derive current state by sorting                  |
| **Single scalar value** (name, count, flag)                | Register (default)                                                               | LWW: each field resolves independently                                                 |
| **Timestamp recording "when"**                             | Register                                                                         | LWW: latest value wins                                                                 |
| **Ordered list, single writer**                            | Array                                                                            | Whole-value LWW: safe when only one device writes                                      |
| **Ordered list, multi-writer**                             | Timestamp-keyed map ([audit log](#audit-logs-preserving-every-state-transition)) | Add-wins preserves all entries; derive order by sorting keys                           |
| **Full-document writes**                                   | Use `UPDATE_LOCAL_DIFF`                                                          | Only changed fields get new timestamps; concurrent edits to other fields are preserved |

## Incremental Adoption

If you're migrating an existing application, you don't have to convert everything at once. A staged approach works well:

1. **Start simple.** Use Ditto's default merge behavior as-is. Arrays work well for single-writer collections. If a collection might occasionally see concurrent writes, be aware that whole-value LWW means one writer's changes silently win — there is no error or notification.
2. **Convert where it matters.** For any collection where multiple devices write concurrently, convert arrays to maps. Ditto's add-wins merge ensures all changes are preserved.

Keep the same collection name and `_id` structure throughout so documents stay compatible. Only the internal structure of nested fields evolves.

<Info>
  This staged approach lets you start getting value from Ditto's sync, offline support, and peer-to-peer mesh immediately, then optimize your data model incrementally based on real usage patterns.
</Info>
