Adopting ACID Transactions
|
Preview | Unofficial | For review only This guide documents preview transaction behavior. Treat the examples and limits as draft guidance. |
Cassandra 6 introduces multi-partition ACID transactions powered by the Accord consensus protocol (CEP-15). These transactions let you atomically read and write across multiple partitions and tables within a single CQL statement block, with serializable isolation.
This guide is for application developers evaluating or adopting ACID transactions. It covers when to use them, how to configure tables, common code patterns, driver behavior, known limitations, and a migration path from Lightweight Transactions (LWT).
For the full CQL syntax reference, see BEGIN TRANSACTION Reference. For cluster-level enablement and table migration procedures, see Onboarding to Accord.
What ACID Transactions Change in Application Design
ACID transactions are a correctness feature, not a general replacement for normal Cassandra writes. They let you move multi-step coordination logic out of the application and into Cassandra when the workflow truly needs atomicity and serializable isolation.
Typical examples include:
-
Transfers and reservations where two or more rows must change together
-
Inventory and quota enforcement where a read must guard a write
-
Workflow state transitions where several tables must remain in sync
-
Uniqueness or ownership checks that depend on data outside one partition
If your application already models a workflow as independent writes plus compensating actions, ACID transactions can often simplify that logic. If the workflow is naturally a single-partition write, keep it that way.
When to Use ACID Transactions
Use ACID transactions when you need
-
Atomic writes across multiple partitions or tables — for example, debiting one account and crediting another as a single indivisible operation.
-
Conditional updates based on reads from multiple partitions — reading a row from one table and using its value to guard a write to a different table or partition.
-
Consistent read snapshots across partitions — ensuring that a set of reads within a transaction all observe the same logical point in time.
Do not use ACID transactions when
-
A single-partition write is sufficient — writes that touch only one partition are simpler and faster without a transaction.
-
You are doing bulk data loading — the per-transaction Accord overhead makes transactions a poor fit for high-throughput ETL or batch ingestion.
-
Eventual consistency is acceptable — if stale reads are harmless for your use case, the consistency guarantees of transactions are unnecessary overhead.
-
You only need single-partition conditional writes — LWT (
INSERT … IF NOT EXISTS,UPDATE … IF) still works in Cassandra 6 and remains the right tool for single-partition check-and-set operations.
|
When in doubt, start with the simplest approach that meets your correctness requirements. ACID transactions are powerful but carry a coordination cost. Reserve them for the workflows that genuinely require multi-partition atomicity. |
ACID Transactions vs. Lightweight Transactions (LWT)
| Feature | LWT (IF EXISTS / IF NOT EXISTS) |
ACID Transactions (BEGIN TRANSACTION) |
|---|---|---|
Partition scope |
Single partition only |
Multiple partitions and tables |
Atomicity |
Single CQL statement |
Multiple statements within one block |
Read-then-write |
No — condition is on the same row being written |
Yes — |
Protocol |
Paxos (4 round trips typical) |
Accord (fewer round trips in most cases) |
Counter columns |
Supported via separate counter tables |
Not supported inside a transaction |
Syntax |
|
|
Table requirement |
None |
|
|
LWT still works in Cassandra 6. Migration to ACID transactions is recommended for new multi-partition use cases but is not required for existing LWT code. If your current LWT patterns are single-partition and working correctly, there is no urgency to change them. |
Enabling ACID Transactions on Tables
Tables must explicitly opt in to participate in Accord transactions.
Set the transactional_mode property to 'full' when creating the table or when altering an existing one.
New table
CREATE TABLE accounts (
account_id text PRIMARY KEY,
balance decimal,
last_updated timestamp
) WITH transactional_mode = 'full';
Existing table
ALTER TABLE accounts WITH transactional_mode = 'full';
|
Setting |
Every table accessed inside a BEGIN TRANSACTION block — for reads or writes — must have transactional_mode = 'full' (or 'mixed_reads' for tables where non-transactional reads are also needed).
A transaction that references a table with transactional_mode = 'off' will fail at execution time with Accord transactions are disabled on table.
|
Common Patterns
Pattern 1: Conditional Insert (Replace IF NOT EXISTS)
The single-partition LWT pattern INSERT INTO users … IF NOT EXISTS becomes a multi-step transaction when you need to check existence and insert atomically:
BEGIN TRANSACTION
LET existing = (SELECT * FROM users WHERE user_id = 'alice');
IF existing IS NULL THEN
INSERT INTO users (user_id, name, email)
VALUES ('alice', 'Alice', 'alice@example.com');
END IF
COMMIT TRANSACTION;
The LET clause reads the row at transaction time.
If existing IS NULL (no row found), the insert proceeds.
The entire operation is atomic — no other transaction can insert the same user_id between the read and the write.
Pattern 2: Multi-Table Atomic Write (Transfer Between Accounts)
This is the canonical case where LWT cannot help: the condition spans two partitions.
BEGIN TRANSACTION
LET source = (SELECT balance FROM accounts WHERE account_id = 'acct1');
LET dest = (SELECT balance FROM accounts WHERE account_id = 'acct2');
IF source.balance >= 100.00 THEN
UPDATE accounts SET balance = source.balance - 100.00
WHERE account_id = 'acct1';
UPDATE accounts SET balance = dest.balance + 100.00
WHERE account_id = 'acct2';
END IF
COMMIT TRANSACTION;
Both UPDATE statements commit atomically.
If source.balance is below 100.00, neither update runs.
No intermediate state where one account is debited but the other is not yet credited is ever visible to other clients.
Write values can reference LET-bound column values using <name>.<column> notation, as shown above with source.balance and dest.balance.
This is the read-before-write pattern that enables conditional arithmetic on the current row values.
|
Pattern 3: Read-Modify-Write with Optimistic Version Check
Use a version column to implement optimistic concurrency control:
BEGIN TRANSACTION
LET current = (SELECT version, data FROM documents WHERE doc_id = 'doc1');
IF current.version = 42 THEN
UPDATE documents SET version = 43, data = 'new content'
WHERE doc_id = 'doc1';
END IF
COMMIT TRANSACTION;
The transaction succeeds only if the document is still at version 42 when it commits.
If another client updated the document concurrently, the IF condition fails and no write occurs — your application can then retry with fresh data.
Pattern 4: Write-Only Atomic Multi-Partition Write
When you need atomicity across partitions but do not need a conditional, omit LET and IF:
BEGIN TRANSACTION
INSERT INTO purchases (user_id, order_id, total)
VALUES ('user1', 'order-99', 49.99);
UPDATE inventory SET stock = stock - 1
WHERE product_id = 'sku-7';
COMMIT TRANSACTION;
Both writes commit atomically with no condition check.
|
This pattern uses |
Modeling Guidance
Use ACID transactions to protect invariants, not to avoid data modeling.
-
Keep the set of touched partitions small and well understood.
-
Keep the transaction body focused on one business action.
-
Prefer one transaction per user-visible state change rather than long, catch-all transaction blocks.
-
Do not use transactions to emulate ad hoc joins or reporting queries.
-
Continue to model for your primary read paths first; transactions add correctness, not arbitrary query flexibility.
|
A good transaction usually reads a few rows, checks one or two business conditions, and writes a few rows. If the block starts to look like a batch job or a mini stored procedure, the design likely needs to be simplified. |
Driver Interaction
Sending a transaction
The driver treats the entire BEGIN TRANSACTION … COMMIT TRANSACTION block as a single CQL request.
Submit the complete block as one string using your driver’s session.execute() or equivalent:
String txn = """
BEGIN TRANSACTION
LET existing = (SELECT * FROM users WHERE user_id = ?);
IF existing IS NULL THEN
INSERT INTO users (user_id, name, email) VALUES (?, ?, ?);
END IF
COMMIT TRANSACTION
""";
session.execute(SimpleStatement.newInstance(txn, userId, userId, name, email));
Bind variables (?) work inside transaction bodies following the same rules as standalone CQL statements.
Prepared statements
Do not use prepared statements for BEGIN TRANSACTION blocks.
Send the full transaction body as a plain request string through your driver’s normal execution API.
Source: TransactionStatement.java eligibleAsPreparedStatement() returns false.
Error handling
Transactions can fail at several points. Handle each category:
-
InvalidRequestExceptionat prepare time — syntax errors, missing partition key columns, unsupported clauses (ORDER BY,GROUP BY,USING TTL, counter columns). These are programming errors; fix the CQL. -
InvalidRequestExceptionat execution time — Accord disabled globally, table not intransactional_mode = 'full', or unsupported consistency level. These are configuration errors; fix the cluster or table setup. -
Transaction rejection — the
IFcondition evaluated to false (no writes occurred, but this is not an error — check the result set to determine whether writes ran). -
Timeout — transactions have separate timeout settings from regular queries. The default transaction timeout may differ from your regular
read_request_timeout_in_msandwrite_request_timeout_in_ms. Consult your operator for the configured timeout values.
Consistency levels
Accord transactions support ONE, QUORUM, SERIAL, and ALL for reads and commits.
|
The following consistency levels are not supported by Accord and will be rejected at execution time: |
Limitations and Current Constraints
The following restrictions apply as of Cassandra 6 (Accord, CEP-15). All are enforced at execution time unless otherwise noted.
| Constraint | Detail |
|---|---|
All tables must have |
Every table accessed inside a transaction (read or write) must have Accord enabled.
A table with |
No counter columns |
Counter columns cannot be read or written inside a transaction. Use a regular numeric column with read-before-write arithmetic instead. |
No |
TTLs and custom timestamps are not allowed on writes inside a transaction. The Accord protocol assigns the timestamp. |
No |
Do not use |
No |
These are not supported inside |
No partition range queries |
|
No nested transactions |
|
No |
If a partition key uses an |
Accord must be enabled cluster-wide |
The cluster must have |
No support during Accord migration |
Multi-partition transactions are rejected while a table is mid-migration away from Accord.
Error: |
| Transaction size limits (maximum reads and writes per transaction) are governed by Accord configuration. Consult your operator for the limits configured in your cluster. |
Migration Path from LWT
Follow this sequence to migrate existing LWT patterns to ACID transactions:
-
Identify LWT usage — search your application code for
IF EXISTS,IF NOT EXISTS, andIF <condition>in CQL statements. Classify each occurrence: is it single-partition (LWT is fine) or multi-partition (candidate for ACID transactions)? -
Enable
transactional_modeon affected tables in staging — runALTER TABLE … WITH transactional_mode = 'full'on the tables involved. This changes the write path. Validate performance and correctness in staging before touching production. -
Rewrite LWT statements as ACID transactions — convert each multi-partition LWT pattern into a
BEGIN TRANSACTION … COMMIT TRANSACTIONblock using the patterns in this guide. -
Test thoroughly — semantics differ — LWT (Paxos) and ACID transactions (Accord) have different failure modes, timeout behaviors, and consistency semantics. A test suite that previously passed with LWT is a starting point, not a guarantee.
-
Deploy to production incrementally — migrate one table or one workflow at a time. Monitor error rates and latency before proceeding.
|
Start by migrating your most complex LWT patterns first — multi-partition or multi-step sequences that currently require application-level coordination logic.
That is where ACID transactions provide the largest correctness benefit and where the migration effort is most justified.
Single-partition |
Testing and Rollout Guidance
Before production rollout, validate more than just basic success cases:
-
Test concurrent writers against the same business entity to confirm your
IFguards behave as expected. -
Test driver timeout and retry behavior specifically for transactions.
-
Verify partial business outcomes never appear when one branch of the transaction should fail.
-
Measure latency separately from normal reads and writes; transaction latency has a different profile.
-
Roll out one transactional workflow at a time so you can attribute regressions to a specific change.
Related Pages
-
BEGIN TRANSACTION Reference — Full CQL syntax, grammar, restrictions, error conditions, and consistency semantics.
-
Choosing Consistency Levels — Guide to Cassandra consistency levels and when to use each.
-
Upgrading to Cassandra 6 — Developer-facing upgrade considerations for Cassandra 6.
-
Onboarding to Accord — Operator guide for enabling Accord, setting
transactional_mode, and managing the migration.