Skip to main content

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.

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 below is the primary tool for implementing read-time derivation.

Document Model

How Ditto documents work internally as CRDTs with automatic conflict resolution.

Data Modeling Tips

Guidance on document size, embedded vs flat models, and structuring collections.

Schema Versioning

Strategies for evolving your schema in a distributed environment.

DQL INSERT

Full reference for INSERT statements including conflict handling clauses.

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 itemsOnly one device writes; others read
Items have a natural or synthetic unique IDOrder matters and the list is never edited concurrently
You want to index into individual itemsSimple list of scalars replaced wholesale

Example: tasks in a project

Model tasks as a map keyed by task ID, and Ditto handles the rest:
{
  "_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.
UPDATE_LOCAL_DIFF is available in SDK 4.12 and later. See Smart Upserts with UPDATE_LOCAL_DIFF for details on how it works.

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.”
Omitting a key from a document write means “no change” — the entry stays. To remove it, use UNSET:
Writing the document WITHOUT t3 does NOT delete it:
{ "tasks": { "t1": { ... }, "t2": { ... } } }

Ditto sees t3 as unchanged, not deleted. It stays in the document.

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.”
A timestamp-keyed audit log automatically preserves the full history:
{
  "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.

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_ininspectingrepair_in_progresscompleted. 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”
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:
{
  "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., inspectingrepair_in_progressinspecting 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.”

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.
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.
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.
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.

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.
Keep related data together — each sub-entity merges independently via its own map key:
{
  "_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.

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.

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.
{
  "_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'.
For full details on configuring permissions rules, see 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.
-- 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.

Quick Reference

Data patternRecommended structureHow Ditto merges it
Collection of entities (items, payments, participants)Map keyed by IDAdd-wins: concurrent additions are all preserved
Status / state machineTimestamp-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”RegisterLWW: latest value wins
Ordered list, single writerArrayWhole-value LWW: safe when only one device writes
Ordered list, multi-writerTimestamp-keyed map (audit log)Add-wins preserves all entries; derive order by sorting keys
Full-document writesUse UPDATE_LOCAL_DIFFOnly 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.
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.