Skip to content

Changes as JSON Patch

Structured changes to a document’s JSON data travel as JSON Patch (RFC 6902) — ordered lists of add, remove, replace, move, copy, and test operations. Patches are compact, human- and agent-readable, and double as the document’s audit trail in the event log.

A patch that was computed against one version of a document may not be safe to apply to a newer version. Writers choose how strict to be, per update:

  • Sequence guard — “apply only if the document is still at sequence N.” The strictest mode: any concurrent change rejects the write.
  • Patch guard — the patch carries RFC 6902 test operations asserting the values it’s about to change. Concurrent changes to other parts of the document are tolerated; a failed test rejects the write.

When generating a patch by diffing, test operations are emitted automatically for replace and remove operations, so the patch defends itself by construction. Additions are the gap — an add has no prior value to assert, so guarded writes catch same-path changes and deletions but not two writers creating the same new key (the add precondition issue).

A rejected guard is expected, not an error — nothing to log and page on — but the write itself is genuinely dropped, not applied. The optimistic entry is rolled back, the client snaps to the winning state, and it’s on the caller to re-derive against that state and retry; nothing replays the lost write automatically. The staged session builds its conflict detection on the same primitive, turning that drop into a reviewable conflict instead.

The array-addressing problem shapes how you model collections. A collection inside a document is stored as records keyed by id, each carrying a fractional index for ordering, rather than as a JSON array. This is deliberate: JSON Patch addresses array elements by position, which is unstable when several writers insert and remove concurrently. Fractional indices make “insert between A and B” a single-key write that doesn’t disturb neighbors. (This is one of the workarounds discussed in JSON Patch RFC issues.)

Why patches — not CRDT operations, not domain events?

Section titled “Why patches — not CRDT operations, not domain events?”

For structured data, datadata deliberately uses server-ordered patches with guards rather than CRDTs — and rather than an application-defined event vocabulary with reducers: a patch is self-applying data, so history can be read and replayed in any context without the application’s code. The trade-offs of both choices are discussed in Design decisions. For collaborative text, where convergence-per-keystroke matters, it uses Yjs instead. Patches carry a real cost here worth naming: a diffed edit to a text field is a whole-value replace, so each change ships and stores the entire string — the event log and the wire bloat fast for large, frequently edited prose. Yjs sends compact per-edit deltas instead, so for that shape of data it’s the better field type. The two travel together in the same update.