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.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.
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.
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 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
- Concept
- DQL
Model tasks as a map keyed by task ID, and Ditto handles the rest: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 explicitUNSET 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.”
- Concept
- DQL
Omitting a key from a document write means “no change” — the entry stays. To remove it, use UNSET:
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.”- Concept
- DQL
A timestamp-keyed audit log automatically preserves the full history: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 writesshippedand another concurrently writesconfirmed, the result isshipped— 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
shippedanddeliveredentries exist.
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”
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: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.”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_modifiedtimestamp) - 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.
- Concept
- DQL
title and Device B only changed assignee, so both edits survive.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.
- Concept
- DQL
Keep related data together — each sub-entity merges independently via its own map key: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.
- Concept
- DQL
_id.region = '1'. Location-scoped: a device sees only its own location’s documents where _id.location = '42'.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.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) | 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:- 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.
- 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.
_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.