Chapter 11 · Transactions, Isolation, Locking, Deadlocks, Retries, and Consistency
ACID Transactions, Auto-Commit vs Explicit Transactions, Statement Boundaries, and Failure Atomicity
Define AtlasMart write units so atomicity, isolation and ambiguous outcomes are visible and testable rather than implicit.
Learning outcomes
AtlasMart must reserve inventory and create a reservation record as one business state transition. If one half commits and the other fails, the graph is internally valid but the business state is wrong. Transactions are therefore not merely syntax around writes: they define what other work can observe, which locks remain held, and which failures are atomic.
Distinguish auto-commit, managed and explicit transactions in the current Python driver.
Explain Neo4j ACID guarantees together with the default read-committed isolation boundary.
Reason about statement boundaries, commit, rollback and failure atomicity.
Observe active transactions without confusing client timeouts with server commit outcomes.
Build a small AtlasMart reservation transaction whose post-commit invariants are testable.
The mandatory lab continues Neo4j Community
2026.07.1, database neo4j, explicit
CYPHER 25 for version-sensitive examples,
container atlasmart-neo4j, authentication
enabled, loopback Bolt/HTTP endpoints, no mandatory APOC/GDS
plugin, and AtlasMart domain identifiers established in
Chapters 01–10. Neo4j 5.26.30 remains the LTS
comparison line. Optional application examples use the
official Python driver 6.3.x.
This generation environment does not run Docker/Neo4j, so no
deadlock frequency, wait duration, retry count, latency, lock
list or cluster routing output is invented. The labs define
deterministic invariants and controlled concurrency procedures
that you execute on the disposable local instance. Community
can use SHOW TRANSACTIONS for its own work;
dbms.listActiveLocks() is currently
Enterprise-only and is therefore optional evidence, not a
mandatory lab dependency.
1. ACID is a transaction contract, not a promise that every application invariant is automatic
| Property | Neo4j transaction meaning | Application responsibility |
|---|---|---|
| Atomicity | All work in a transaction commits or the transaction is rolled back | Put operations that must succeed/fail together in the same transaction |
| Consistency | Committed database state respects database-level integrity rules | Define business invariants with constraints plus transaction logic/tests |
| Isolation | Default is read committed; uncommitted writes are not visible to other transactions | Account for non-repeatable reads and explicit locking needs |
| Durability | Committed work is recoverable through the transactional storage/logging mechanism | Still design backup/recovery and ambiguous-client-outcome reconciliation |
Read committed is deliberately weaker than serializable isolation. A transaction can read data, another transaction can commit a change, and a later read in the first transaction can observe a different committed value. Traversal results are not frozen simply because the transaction remains open.
2. Three client transaction styles solve different problems
| Style | Python driver shape | Retry behavior | Use when |
|---|---|---|---|
| Auto-commit | session.run() |
No managed-transaction callback retry contract | One Cypher statement is the whole unit of work |
| Managed |
session.execute_write(fn) /
execute_read(fn)
|
Driver may invoke the callback more than once for retryable failures | A bounded unit of database work must retry safely |
| Explicit/unmanaged | session.begin_transaction() |
Application controls begin/commit/rollback | You need explicit transaction lifetime/control and accept responsibility for retry policy |
A single Cypher statement is itself transactional. The reason to group multiple statements is business atomicity—not because individual Cypher statements otherwise bypass transactions.
from neo4j import GraphDatabaseURI = "bolt://127.0.0.1:7687"AUTH = ("neo4j", "atlasmart-course-2026")def reserve(tx, reservation_id, qty): record = tx.run(""" MATCH (s:Stock {stockId:'CH11-ST-001'}) WHERE s.onHand - s.reserved >= $qty MERGE (r:Reservation {reservationId:$reservationId}) ON CREATE SET r.quantity=$qty, r.labTag='ch11' MERGE (r)-[:RESERVES]->(s) SET s.reserved = s.reserved + CASE WHEN r.quantity=$qty THEN $qty ELSE 0 END, s.version = s.version + 1 RETURN s.onHand AS onHand,s.reserved AS reserved """, reservationId=reservation_id, qty=qty).single() if record is None: raise ValueError("insufficient stock or reservation conflict") return dict(record)with GraphDatabase.driver(URI, auth=AUTH) as driver: with driver.session(database="neo4j") as session: print(session.execute_write(reserve, "CH11-R-001", 3))
3. Failure atomicity is easiest to prove with a deliberate failure
Create the disposable fixture, begin an explicit transaction, update the stock, then execute a statement that violates the reservation uniqueness constraint. Roll the transaction back and verify that neither the stock counter nor reservation relationship changed. This demonstrates atomicity inside the database; it does not say anything about an email, payment call or message already sent outside Neo4j.
CYPHER 25CREATE CONSTRAINT ch11_stock_id IF NOT EXISTSFOR (s:Stock) REQUIRE s.stockId IS UNIQUE;CREATE CONSTRAINT ch11_reservation_id IF NOT EXISTSFOR (r:Reservation) REQUIRE r.reservationId IS UNIQUE;MERGE (p:Product {productId:'CH11-P-001'})SET p.name='AtlasCam Pro', p.labTag='ch11'MERGE (store:Store {storeId:'CH11-S-001'})SET store.name='Central Store', store.labTag='ch11'MERGE (stock:Stock {stockId:'CH11-ST-001'})SET stock.onHand=20, stock.reserved=0, stock.version=0, stock.labTag='ch11'MERGE (store)-[:HOLDS_STOCK]->(stock)-[:FOR_PRODUCT]->(p);MERGE (a:Counter {counterId:'CH11-A'}) SET a.value=0,a.labTag='ch11';MERGE (b:Counter {counterId:'CH11-B'}) SET b.value=0,b.labTag='ch11';
CYPHER 25MATCH (s:Stock {stockId:'CH11-ST-001'})OPTIONAL MATCH (s)<-[:RESERVES]-(r:Reservation {labTag:'ch11'})RETURN s.onHand AS onHand, s.reserved AS reserved, coalesce(sum(r.quantity),0) AS reservationQuantity, count(r) AS reservationCount, s.reserved <= s.onHand AS capacityInvariant, s.reserved = coalesce(sum(r.quantity),0) AS reconciliationInvariant;
4. Observe transaction lifetime
SHOW TRANSACTIONS exposes running work, including
query identifiers and timing fields available to the current
user/privilege set. Keep a transaction open only long enough to
reproduce the signal; do not introduce arbitrary user/network
waits into a production write transaction.
CYPHER 25SHOW TRANSACTIONSYIELD transactionId,currentQueryId,currentQuery,status,elapsedTime,waitTime,idleTimeRETURN transactionId,currentQueryId,status,elapsedTime,waitTime,idleTime,currentQueryORDER BY elapsedTime DESC;
5. Deliberately wrong: treat a client timeout as proof that the transaction rolled back
A transport timeout or dropped connection can leave the application uncertain whether the server committed. Retrying a non-idempotent operation blindly can therefore duplicate effects. The repair is a stable operation/reservation identifier plus reconciliation: query by the operation ID before deciding whether to retry, compensate or surface an ambiguous outcome.
Check your understanding
- What is Neo4j’s default isolation level?
- Does opening an explicit transaction make reads repeatable by itself?
- Why use a managed transaction callback?
- What does a client timeout prove about commit?
- What makes a reservation workflow auditable?
Review the answers
1. Read committed.
2. No. Read committed still allows non-repeatable reads unless stronger locking is deliberately introduced.
3. It groups database work into a unit the driver can retry for retryable failures, provided the callback is safe to invoke more than once.
4. Only that the client did not receive a conclusive result; it does not necessarily prove commit or rollback.
5. Stable operation identity, atomic graph changes, post-commit invariants and reconciliation for ambiguous outcomes.
Summary and next step
Transactions define atomic state transitions, but concurrency determines how those transitions interact. Next we inspect locks, contention hotspots and deadlock detection.
Authoritative references
- Current Neo4j versions — Release/LTS snapshot used for this chapter.
- Database internals and transactional behavior — ACID behavior, read-committed isolation and transaction internals.
- Database transactions — Transaction lifecycle, memory and completion behavior.
- Concurrent data access — Locks, lost updates, contention, deadlocks and lock timeout semantics.
- Show and terminate transactions — SHOW TRANSACTIONS visibility and transaction diagnostics.
- Neo4j Python Driver 6.3 API — Current official Python driver, Bolt compatibility and transaction APIs.
- Python driver managed transactions — Managed transaction functions, retries and explicit transaction patterns.
- Python driver bookmarks — Bookmark propagation and causal chaining across sessions.