skip to content

What atomicity can you rely on across rows in a wide-column store, and how do you design an operation that must change two rows?

level: seniorimportance: should knowfreq 46%

answer

  1. the atomic unit is small
  2. one row or one partition
  3. batches are not transactions
  4. co-locate, or make it re-drivable

basics

~20 s

Atomicity stops at one row, or one partition in partition-keyed stores. Operations across rows have no isolation and can be partly applied, so put data that must change together under one key, or make multi-row steps idempotent and re-drivable.

solid answer

~40 s

The atomic unit in a wide-column store is small: **one row** in range-served stores, **one partition** in partition-keyed ones, where all changes to a partition in one request apply atomically and in isolation. Across rows or partitions there is no general transaction. Some stores offer a **batch** whose members are guaranteed to be applied eventually, all or none, but readers can see it half-applied, and other stores accept a multi-row request where each row succeeds or fails on its own. So, first, **co-locate**: model data that must change together into one row or partition. Where that is impossible, make each step **idempotent**, record intent first (a durable batch or an event), order the steps so a partial state is harmless, and re-drive or reconcile until everything has landed.

go deeper

for a junior

Know that atomicity covers one row or one partition, not arbitrary rows.

for a middle

Explain what a logged batch does and does not guarantee, and why a multi-row request can be partly applied.

for a senior

Design a two-row change with durable intent, idempotent steps and safe ordering, and add reconciliation for drift.

for a principal

Be ready to decide when multi-row consistency needs justify keeping part of the domain in a transactional database instead.

## How small the unit is Relational databases make multi-row transactions the default. Wide-column stores do the opposite: the **atomic unit is one row or one partition**, because only there is a single place — one owning server, or one replica's copy of a partition — that can apply a change all at once. | store shape | atomic and isolated unit | across units | |---|---|---| | range-served, one sorted row key | a single row, all its families | no general multi-row transaction; a multi-row request succeeds or fails row by row | | partition-keyed, leaderless | all changes to one partition in one request, on each replica | independent writes to different replica sets | Some stores in the family offer an optional mechanism for a group of rows, but none gives relational-style transactions as the normal path. ## Batches are not transactions Several stores accept a **batch** of writes to different rows or partitions. Read the guarantee carefully: - A **logged** batch records the batch durably first and guarantees that, if any part is applied, **all of it eventually is**. - It provides **no isolation**: a reader can observe some members applied and others not. - It is **slower** than independent writes, because the batch log is an extra write and the coordinator does more work. - A multi-row request in range-served stores is usually **not atomic as a whole**: each row's mutation succeeds or fails independently, and the client must retry the failures. So a batch buys eventual completeness, not atomicity in the relational sense. ## Design technique 1: co-locate what changes together The first move is modelling: 1. Put the entity and the data that must change with it **under one key**: one row with several columns, or one partition with several clustering rows. 2. Store a change as **one row of the partition** rather than updates to several entities, when the read path can derive state from it. 3. Accept duplication so each read path's copy lives under one key. ## Design technique 2: make multi-row changes safe to repeat When two keys really must change: 1. **Record intent durably first** — a logged batch, an outbox-style event, or a pending record under its own key. 2. Make every step **idempotent**: writing the same cells again, with deterministic values, changes nothing. 3. **Order steps** so a half-applied state is harmless: write the new detail row before the index pointer that makes it reachable, and remove the old pointer last. 4. **Re-drive** unfinished work from the recorded intent, and run a periodic **reconciliation** that compares the copies and fixes drift. ## When this is the wrong store If the domain needs frequent, isolated changes across many entities — balances moved between accounts, stock reserved against orders — the workarounds grow into a hand-built transaction system. That is a signal to keep that part of the domain in a database with real multi-row transactions. ## Interview angle Say the atomic unit precisely for each shape, distinguish a batch's eventual completeness from isolation, and offer co-location first and idempotent, re-drivable steps second.

  • Why is a logged batch slower than writing the rows separately?
    The coordinator first writes the batch to a durable batch log so it can finish it after a crash, then applies the members and cleans up the log. That extra write and bookkeeping cost latency; the benefit is eventual completeness, not speed.
  • How do you order writes when adding a detail row and an index row that points to it?
    Write the detail row first, then the index entry. If the process stops in between, you are left with an unreachable detail row, which a cleanup job can remove, rather than an index entry pointing at nothing.

saying these in an interview costs you the question

  • Assuming a batch of writes to several partitions is isolated like a transaction
  • Believing a multi-row request succeeds or fails as one unit
  • Spreading data that must change together across many keys
  • Using batches to speed up unrelated writes to different partitions
  • Retrying multi-row steps that are not idempotent