skip to content

In an OR-Set (observed-remove set) CRDT, how are add and remove operations tracked so that a concurrent add and remove of the same element resolves correctly, and why does a naive two-phase-set (an add-set plus a tombstone remove-set) fail on this case?

level: seniorimportance: must knowfreq 55%

answer

  1. unique tag per add, not per element name
  2. tombstone only tags you've observed
  3. presence = has an untombstoned tag
  4. add wins on concurrent add/remove
  5. 2P-Set permanently blocks re-adds

basics

~30 s

An OR-Set tags every 'add' with a unique ID, and a 'remove' only cancels the specific adds it has actually seen. So if one replica adds an item while another replica, not knowing about that add yet, tries to remove the same-named item, the new add survives because its unique tag was never observed by that remove. A simpler design that just marks a name as 'removed forever' would wrongly block that later add too.

solid answer

~50 s

OR-Set represents each add as an (element, unique-tag) pair placed in an 'adds' set; a remove doesn't delete the element name generically - it moves into a 'tombstones' set only the specific tags that replica has currently observed for that element. An element is present if it has at least one tag in adds that is not in tombstones. Merge is simply set-union on both the adds set and the tombstones set, trivially commutative/associative/idempotent. Because remove only tombstones tags it has actually seen, a concurrent add (new tag, not yet observed) survives any remove that raced against it - 'add wins' on concurrent add/remove of the same element. A 2P-Set (grow-only add-set plus grow-only tombstone-set keyed by element name, not by unique tag) fails this because once an element name is tombstoned, it can never be re-added - the tombstone set has no notion of 'which specific add' it cancels, so it permanently blocks the element identity, even future genuinely new adds.

go deeper

for a junior

Should get the basic idea that removing something needs to somehow 'know about' the specific add it's canceling, rather than banning the name outright, even if the tag mechanism isn't precise yet.

for a middle

Should describe the (element, unique-tag) representation and state the 'add wins' outcome for concurrent add/remove.

for a senior

Should explain precisely why 2P-Set fails (tombstone keyed by name, not instance) and reason about tombstone/tag metadata growth as a real operational cost.

for a principal

Should know that safe tombstone GC requires causal-stability tracking across all replicas, and be able to weigh OR-Set's metadata cost against alternative designs for a given churn pattern.

## How an OR-Set represents membership An **OR-Set** (Observed-Remove Set) represents set membership not as raw elements but as pairs of `(element, unique-tag)`, where the tag is generated fresh (e.g. a UUID, or a `(replica-id, counter)` pair) every time an element is added, even if the same element value was added before. Internally the CRDT keeps two collections: - an **adds** set of `(element, tag)` pairs ever added; - a **tombstones** set of tags that have been removed. An element is a current member of the set if and only if it has at least one `(element, tag)` pair in adds whose tag is not present in tombstones. - **To add** an element, a replica generates a new unique tag and inserts `(element, tag)` into its local adds set. - **To remove** an element, a replica looks at every tag currently associated with that element in its local adds view — every add of that element it has personally observed — and copies exactly those tags into tombstones; it does not tombstone the element name in the abstract, only the specific tags it has actually seen. Merging two OR-Set replicas is simple set union on both adds and tombstones independently, which is trivially **commutative, associative, and idempotent**. ## Concurrent add versus remove: why 'add wins' This design resolves a fundamentally ambiguous case: what should happen when one replica removes an element while, concurrently and without knowledge of that remove, another replica adds the same element value? A **2P-Set** (two-phase set: one grow-only add-set of raw elements, one grow-only tombstone-set of raw elements, with "once tombstoned, always tombstoned") resolves this by fiat — it always favors remove, permanently: once an element name is in the tombstone set, no future add of that same value can ever bring it back, because presence is defined as "in adds AND NOT in tombstones," and tombstones only grow. That's often the wrong answer: if user X removes "urgent" from a shared list at the same moment user Y adds a fresh "urgent" item they intend to keep, a 2P-Set would silently and permanently block "urgent" forever, even though Y's add should reasonably survive. OR-Set fixes this by keying removal to specific observed instances, not the element's identity in the abstract, so a concurrent add — which produces a fresh tag the concurrent remove could not possibly have observed — is never tombstoned by that remove and survives merge. This is generally summarized as **add wins** semantics. ## What that correctness costs The cost of this correctness is metadata: - every element carries a growing set of tags (one per add ever performed on that value, even if most were later removed); - every remove leaves behind a permanent tombstone entry that must be retained as long as any not-yet-synced replica might still hold an unobserved copy of that tag's add. For a set that sees heavy churn on the same element value (e.g. a naive "currently online" flag implemented as a set), tombstone volume can grow far larger than the set's actual live contents, and every replica pays the space and merge-time cost of carrying that history indefinitely unless it is actively garbage-collected. ## The two failure modes 1. The most common production failure mode is exactly the **2P-Set trap** described above, usually discovered the hard way: a team builds a "tags on a document" feature as add-set plus remove-tombstone-set keyed by tag name, ships it, and then gets support tickets about a tag that "can never be re-added" after being removed once — the tombstone set has permanently blacklisted that tag name. 2. A second failure mode specific to OR-Set itself is **unbounded tombstone growth** when garbage collection is skipped entirely: without a safe GC protocol (only pruning a tombstone once every replica has provably observed it, which requires tracking causal stability across all replicas), the tag/tombstone metadata keeps growing forever, degrading merge time and storage regardless of how small the logically "current" set actually is. ## Where it ships - **Riak KV's** built-in Set CRDT type is an OR-Set variant, used for cases like a set of a document's collaborators or feature flags enabled per tenant that must accept concurrent adds/removes across geo-distributed nodes without a coordinator. - The **AntidoteDB** CRDT-native database likewise offers an OR-Set as one of its primary collection types, explicitly chosen over a 2P-Set for its add-wins semantics.

  • How does an OR-Set know which tags to tombstone if a replica hasn't seen every existing add for that element?
    It only tombstones the tags present in its own local adds view at the moment of the remove - if it hasn't observed some concurrent add elsewhere yet, that add's tag simply isn't available to tombstone, which is exactly the mechanism that lets a concurrent add survive; there's no attempt to guess at tags the replica hasn't seen.
  • What does OR-Set cost in terms of state size compared to a plain set?
    Each element effectively costs one tag per add operation ever performed on it (until safely garbage-collected), plus the tombstone entries for removed tags, so a heavily churned set can carry many times the metadata of its actual live element count - a plain set costs O(current elements) while an unpruned OR-Set costs O(total adds and removes ever performed).
  • Is there a way to bound tombstone growth without unsafe data loss?
    Yes, via causal-stability tracking: a tombstone can be safely pruned only once every replica has acknowledged having observed it (so no replica could still hold a stale add referencing that tag), which typically requires a periodic exchange of 'what have you seen' vectors among replicas - reintroducing a coordination step purely for garbage collection, even though the writes themselves stayed coordination-free.

Like a nightclub guest list where every entry stamp gets a unique ticket number, even for the same person entering twice on different nights - crossing someone off only cancels the specific ticket numbers the bouncer has actually seen that person use, so if the person got a brand-new ticket from a different door at the same moment, that fresh ticket still gets them in, instead of the whole name being permanently banned.

saying these in an interview costs you the question

  • Proposes a remove-tombstone-set keyed by element name/value instead of by unique tag
  • Can't explain why 2P-Set permanently blocks re-adding a removed element
  • Assumes OR-Set tombstones can be deleted immediately after a remove
  • Doesn't recognize 'add wins' as OR-Set's defining semantic for concurrent add/remove

context