Chapter 16 · Lightweight Transactions, Paxos, CAS, and Linearizable Conditional Updates
IF NOT EXISTS and IF Conditions: Compare-and-Set Semantics in CQL
Use CQL IF NOT EXISTS and IF conditions as observable compare-and-set operations, then prove winner/loser behavior under concurrent contention.
Learning outcomes
AtlasMart must guarantee that only one account can claim a promotional code and that an inventory reservation is accepted only when the expected state still holds. A normal last-write-wins update can overwrite a concurrent decision. This lesson introduces Cassandra's narrow tool for that problem: a Paxos-backed conditional mutation that reports whether the condition was actually applied.
Explain IF NOT EXISTS and IF conditions as compare-and-set, not as ordinary upserts with extra syntax.
Interpret the [applied] result and current-value columns returned by failed conditions.
Separate serial consistency for the Paxos phase from regular consistency for the learned mutation.
Run concurrent claim attempts and prove that one logical winner is chosen for the protected key.
Recognize boundaries: LWT is not a cross-partition relational transaction and custom client timestamps are not allowed for conditional updates.
The mandatory labs continue the disposable AtlasMart course
cluster: Apache Cassandra 5.0.9 in the pinned
cassandra:5.0.9 image, Java 17 inside the image,
cluster atlasmart-course, Docker network
atlasmart-cassandra, nodes
atlasmart-cass-1..3, datacenter dc1,
racks rack1..rack3, 16 virtual nodes per node,
NetworkTopologyStrategy with replication factor
(RF) 3, and regular consistency level (CL)
LOCAL_QUORUM unless an experiment says otherwise.
New tables use UnifiedCompactionStrategy (UCS),
gc_grace_seconds = 864000 unless explicitly
isolated for an exercise, and no default TTL. Authentication,
client TLS, internode TLS, and remote JMX are disabled only
inside this isolated local learning network. The optional
application examples use Apache Cassandra Java Driver
4.19.3. Verify your actual runtime with
nodetool version, cqlsh --version,
and java -version. This lesson uses only one DC,
so SERIAL and LOCAL_SERIAL have
similar replica scope here; Lesson 3 adds the multi-DC
distinction.
Run commands only against the disposable Apache Cassandra course lab or another explicitly approved non-production environment. Confirm node, keyspace, table, container, volume, path, and datacenter targets before destructive, failure-injection, cleanup, repair, restore, security, or topology operations. Capture current state and expected rollback/recovery evidence first; output and timings can differ by host, operating system, Java runtime, Docker/runtime, driver, and Cassandra configuration.
Core terms for this chapter
A lightweight transaction (LWT) is Cassandra's
conditional mutation mechanism. It uses the
Paxos consensus protocol so competing
operations on the same logical Paxos scope can agree on one
ordered outcome. Compare-and-set (CAS) means
“apply this mutation only if the current value satisfies the
condition.” A conditional mutation is CQL such
as INSERT ... IF NOT EXISTS or
UPDATE ... IF column = value.
Linearizable means successful operations appear
to occur in a single real-time-compatible order within the
guarantee's documented scope; it is stronger than ordinary
eventual consistency.
A coordinator is the Cassandra node handling one request. A replica is a node that stores the partition according to the keyspace replication strategy. A partition groups rows by partition key and maps through the partitioner to a token; the token determines natural replicas. RF is replication factor. A regular CL controls the data/learn phase of the operation. SERIAL and LOCAL_SERIAL are serial consistency levels that control the Paxos phase. A ballot is the proposal identity/order used by Paxos rounds. Contention occurs when concurrent conditional operations compete for the same Paxos state. A hot partition is a partition receiving disproportionately high request volume. A retry repeats an operation after an error or timeout; for LWT, an ambiguous outcome must be reconciled before blind retry. SSTables are immutable on-disk table files, while compaction rewrites SSTables. repair is Cassandra's anti-entropy process and is separate from Paxos agreement. Storage-Attached Indexing (SAI) and vector search can help locate rows but do not create uniqueness or cross-row transactional invariants.
1. What [applied] actually means
INSERT ... IF NOT EXISTS asks Cassandra to install
the row only if the target primary key is absent at the
linearization point. UPDATE ... IF ... compares
current values and applies the mutation only if the predicate is
satisfied. Cassandra returns a special result whose
[applied] column is True for the
winner and False for a rejected condition. On
rejection, Cassandra can return current values needed to
understand why the compare failed. This is not a SQL transaction
block: the guarantee is attached to the conditional operation
and its Paxos scope.
| Operation | Condition | Successful result | Rejected result |
|---|---|---|---|
INSERT ... IF NOT EXISTS |
target primary key absent | [applied]=True and row created |
[applied]=False; existing row remains
|
UPDATE ... IF revision=7 |
current revision exactly 7 | new values become chosen mutation | condition false; no requested mutation |
normal INSERT/UPDATE |
none | last-write-wins mutation accepted by normal write path | no CAS winner/loser decision |
2. Reproduce a unique claim and a compare-and-set update
docker exec atlasmart-cass-1 nodetool versiondocker exec atlasmart-cass-1 nodetool statusdocker exec atlasmart-cass-1 java -versiondocker exec atlasmart-cass-1 cqlsh -e "SELECT cluster_name, data_center, rack, release_version FROM system.local;"docker exec atlasmart-cass-1 cqlsh -e "SELECT peer, data_center, rack, release_version FROM system.peers_v2;"# Continue only when all three nodes are UN in dc1.# If the course cluster is absent, recreate it with the same Chapter 01 conventions# and pinned cassandra:5.0.9 image before running this chapter.
CREATE KEYSPACE IF NOT EXISTS atlasmart_lwtWITH replication = {'class':'NetworkTopologyStrategy','dc1':3};CREATE TABLE IF NOT EXISTS atlasmart_lwt.inventory_guard ( sku text PRIMARY KEY, available int, reservation_owner text, revision int) WITH compaction = {'class':'UnifiedCompactionStrategy'};CREATE TABLE IF NOT EXISTS atlasmart_lwt.unique_claim ( claim_type text, claim_value text, owner_id text, created_at timestamp, PRIMARY KEY ((claim_type, claim_value))) WITH compaction = {'class':'UnifiedCompactionStrategy'};CREATE TABLE IF NOT EXISTS atlasmart_lwt.order_projection ( order_id text PRIMARY KEY, customer_id text, state text, updated_at timestamp) WITH compaction = {'class':'UnifiedCompactionStrategy'};CONSISTENCY LOCAL_QUORUM;SERIAL CONSISTENCY LOCAL_SERIAL;INSERT INTO atlasmart_lwt.inventory_guard(sku, available, reservation_owner, revision)VALUES ('sku-42', 10, 'NONE', 0);
CONSISTENCY LOCAL_QUORUM;SERIAL CONSISTENCY LOCAL_SERIAL;DELETE FROM atlasmart_lwt.unique_claim WHERE claim_type='promo' AND claim_value='WELCOME-42';INSERT INTO atlasmart_lwt.unique_claim(claim_type, claim_value, owner_id, created_at)VALUES ('promo','WELCOME-42','customer-a',toTimestamp(now()))IF NOT EXISTS;-- Re-run with a different owner. The second contender should not overwrite the winner.INSERT INTO atlasmart_lwt.unique_claim(claim_type, claim_value, owner_id, created_at)VALUES ('promo','WELCOME-42','customer-b',toTimestamp(now()))IF NOT EXISTS;SELECT * FROM atlasmart_lwt.unique_claimWHERE claim_type='promo' AND claim_value='WELCOME-42';
UPDATE atlasmart_lwt.inventory_guardSET available=9, reservation_owner='order-9001', revision=1WHERE sku='sku-42'IF available=10 AND revision=0;-- This stale compare must be rejected instead of silently overwriting order-9001.UPDATE atlasmart_lwt.inventory_guardSET available=9, reservation_owner='order-9002', revision=1WHERE sku='sku-42'IF available=10 AND revision=0;SELECT * FROM atlasmart_lwt.inventory_guard WHERE sku='sku-42';
On an untouched fixture, the first condition should report
[applied]=True and the stale duplicate should
report False. The exact columns printed on a
rejected condition, trace events, host addresses, and timing
depend on Cassandra/cqlsh version and current state. Capture
your own output and verify the final row.
3. Concurrent CAS contenders
Sequential replays prove condition semantics but not contention.
The following local PowerShell exercise starts several
independent cqlsh clients that all compete to change one row
from UNCLAIMED. Only the operation whose condition
is chosen while the expected state is still current should be
applied. Job scheduling can change which worker wins; winner
identity is intentionally nondeterministic.
CREATE TABLE IF NOT EXISTS atlasmart_lwt.claim_once ( resource_id text PRIMARY KEY, owner text) WITH compaction = {'class':'UnifiedCompactionStrategy'};INSERT INTO atlasmart_lwt.claim_once (resource_id, owner)VALUES ('flash-sale-slot','UNCLAIMED');
$jobs = 1..8 | ForEach-Object { $n = $_ Start-Job -ArgumentList $n -ScriptBlock { param($n) docker exec atlasmart-cass-1 cqlsh -e "CONSISTENCY LOCAL_QUORUM; SERIAL CONSISTENCY LOCAL_SERIAL; UPDATE atlasmart_lwt.claim_once SET owner='worker-$n' WHERE resource_id='flash-sale-slot' IF owner='UNCLAIMED';" }}$jobs | Wait-Job | Receive-Job$jobs | Remove-Jobdocker exec atlasmart-cass-1 cqlsh -e "SELECT * FROM atlasmart_lwt.claim_once WHERE resource_id='flash-sale-slot';"
4. Wrong approach: “make every write an LWT”
If AtlasMart wraps ordinary telemetry inserts, order
projections, cache-like session updates, and append-only events
in IF conditions “for safety,” every write pays
Paxos coordination even when there is no invariant to arbitrate.
That increases network round trips, coordinator work, tail
latency, and contention exposure. The repair is architectural:
reserve LWT for keys where two valid-looking concurrent
decisions must be serialized, and keep idempotent/normal writes
on the regular path.
Conditional updates reject CQL USING TIMESTAMP.
Through the Java driver, client-generated timestamps are
ignored for LWT and the server assigns the timestamp. Do not
design CAS correctness around an application-supplied
microsecond timestamp.
SELECT * FROM atlasmart_lwt.unique_claim WHERE claim_type='promo';SELECT * FROM atlasmart_lwt.inventory_guard WHERE sku='sku-42';SELECT * FROM atlasmart_lwt.claim_once WHERE resource_id='flash-sale-slot';-- Reset only the disposable Chapter 16 fixtures when you want to repeat the lab.DELETE FROM atlasmart_lwt.unique_claim WHERE claim_type='promo' AND claim_value='WELCOME-42';UPDATE atlasmart_lwt.inventory_guard SET available=10, reservation_owner='NONE', revision=0 WHERE sku='sku-42';UPDATE atlasmart_lwt.claim_once SET owner='UNCLAIMED' WHERE resource_id='flash-sale-slot';
5. Verification checklist
- Record regular CL and serial CL separately.
-
Capture at least one
[applied]=Trueand one[applied]=Falseresult. - Run concurrent contenders and verify only one final owner exists.
- Explain why a rejected condition is a normal business outcome, not automatically an infrastructure error.
- Explain why the same mechanism does not make two unrelated partitions one relational transaction.
Check your understanding
- What does [applied]=False mean?
- Why are regular CL and serial CL separate?
- Does IF NOT EXISTS make an arbitrary query result globally unique?
- Can a client force USING TIMESTAMP on a conditional update?
- Why is “LWT for every write” usually a poor default?
Review the answers
1. The condition did not hold at the linearizable compare point, so Cassandra did not apply the requested conditional mutation; it is often an expected contention/business result.
2. Serial CL scopes the Paxos phase; regular CL controls the learned/data mutation visibility requirements after consensus.
3. No. It protects the conditional primary-key scope you model; a search/index query is not a uniqueness lock.
4. No. CQL rejects custom timestamps for conditional updates, and driver timestamp generators are ignored for LWT.
5. It pays consensus coordination and contention costs even when no true invariant requires arbitration.
Production judgment
LWT is an invariant tool, not a “strong consistency” checkbox
for every write. Before using it, define the exact invariant,
its partition/key scope, contender cardinality, expected
concurrency, RF and datacenter placement, regular and serial CL,
failure behavior, acceptable p95/p99 latency,
retry/reconciliation rules, and how a timed-out outcome will be
discovered. Measure CASRead/CASWrite
latency, timeouts, failures, unavailables, condition-not-met
counts, and contention histograms alongside ordinary request
latency, CPU, JVM garbage collection, network, disk, compaction,
repair state, and hot-key distribution.
Do not infer a universal LWT throughput number from this laptop
lab. SAI/vector indexes do not make a search result safe as a
uniqueness lock; security/tenant boundaries require
authorization and data-model controls, not
LOCAL_SERIAL. Managed Cassandra services can
constrain JMX, Paxos variants, or topology settings, so
translate the same invariant and evidence model to the service's
supported telemetry. Migration and rollback must account for
application semantics: replacing a normal write with LWT can
change latency and availability, while removing LWT can silently
weaken an invariant. Lesson 2 opens the Paxos mechanism and
compares its round trips and CAS latency evidence with the
regular write path.
Summary and next bridge
Cassandra conditional statements are compare-and-set operations with explicit winner/loser evidence. They solve a narrow concurrency problem; they do not create a general relational transaction manager. Next, follow the Paxos phases and measure why LWT needs more coordination than a regular write.
Authoritative references
Use these current official sources as the version-sensitive source of truth. Re-check them when regenerating this chapter because Paxos variants, driver behavior, metrics, and operational recommendations can evolve.
- Apache Cassandra downloads — 5.0.9 and Java Driver 4.19.3
- Cassandra guarantees — LWT and linearizable consistency
- cqlsh consistency and serial consistency
- cassandra.yaml — paxos_variant
- Cassandra monitoring metrics — CASRead/CASWrite
- nodetool proxyhistograms — CAS latency distributions
- Java Driver statement attributes — regular and serial consistency
- Java Driver retries and idempotence
- Java Driver query timestamps — LWT restriction