Transactions and drivers

This section documents the current Neo4j 2026.x calendar-versioned line — Neo4j moved from semantic to calendar versioning (YYYY.MM) in 2025, and the same line applies to the Graph Data Science library — as published at the Neo4j documentation, which is the reference these pages are written and verified against. No specific monthly patch is pinned. Some areas (Aura’s internal infrastructure, the Raft consensus implementation details, and the GDS Pregel API’s low-level internals) are linked, not documented in depth.

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production, as Neo4j iterates quickly.

This section’s bibliography lists the reference material consulted while preparing these pages.

Every write to Neo4j runs inside a transaction, whether it is issued from cypher-shell, Neo4j Browser, or one of the official drivers. This page covers what that transaction guarantees, how a cluster keeps reads consistent with prior writes, and the driver-level API — shared by every official language driver — used to run transactions from application code.

ACID guarantees

A Neo4j transaction is fully ACID: Atomic (all its writes commit or none do), Consistent (constraints are enforced before commit), Isolated (concurrent transactions do not see each other’s uncommitted writes — Neo4j runs at read-committed isolation, taking write locks on the nodes and relationships it touches until commit), and Durable (a committed transaction survives a crash, once it is flushed to the transaction log). This holds for a single-instance Community Edition server exactly as it does for a multi-member Enterprise cluster — clustering adds replication on top, not a weaker guarantee. See Java driver: Concurrency for how concurrent transactions interact under this isolation level.

// Everything below either all commits or none of it does: an ACTED_IN relationship
// is never left pointing at a Person that failed to be created.
CREATE (p:Person {name: "Hugo Weaving", born: 1960})
CREATE (m:Movie {title: "The Matrix"})
CREATE (p)-[:ACTED_IN {roles: ["Agent Smith"]}]->(m);

Causal consistency and bookmarks

An Enterprise cluster replicates data from a primary to one or more secondary members; a secondary can lag the primary by a small amount. Reading from a secondary right after writing to the primary can therefore miss the write — unless the driver is told to wait. Every transaction the driver runs returns a bookmark, an opaque token marking how far that transaction’s writes have propagated. Passing the previous transaction’s bookmark into the next session tells the server "do not run this transaction until you have caught up to at least this point", giving the application read-your-writes consistency across the cluster without pinning every read to the primary.

Bookmark writeBookmark;
try (Session writeSession = driver.session(SessionConfig.forDatabase("neo4j"))) {
    writeSession.executeWrite(tx ->
        tx.run("CREATE (:Person {name: $name})", Map.of("name", "Trinity")));
    writeBookmark = writeSession.lastBookmarks();
}

// A read on another session -- possibly routed to a secondary -- is guaranteed to
// see the write above once it is chained from that bookmark.
try (Session readSession = driver.session(
        SessionConfig.builder().withBookmarks(writeBookmark).build())) {
    readSession.executeRead(tx ->
        tx.run("MATCH (p:Person {name: $name}) RETURN p", Map.of("name", "Trinity")).list());
}

Bookmarks matter only across sessions — a single session already chains its own transactions causally. See Java driver: Transactions for the full bookmark and session-chaining API.

The Bolt driver family

Bolt is Neo4j’s binary application protocol for client-server communication — the same protocol underlying cypher-shell and Neo4j Browser (see Getting started with Neo4j). Neo4j publishes an official driver over Bolt for each of five languages — Java, Python, JavaScript, .NET, and Go — and all five expose the same core shape: a Driver created once from a connection URI and credentials, opening short-lived Session objects that each run one or more transactions.

// https://neo4j.com/docs/java-manual/current/connect/
Driver driver = GraphDatabase.driver(
    "neo4j://localhost:7687",
    AuthTokens.basic("neo4j", "change-me-please"));
driver.verifyConnectivity();
// The Driver is thread-safe and expensive to create: build one per application
// process and close it on shutdown, opening/closing Sessions per unit of work.

Rather than hand-writing BEGIN/COMMIT/ROLLBACK and retry logic, every driver exposes transaction functions: executeWrite/executeRead in Java, .NET and JavaScript, execute_write/execute_read in Python, and their Go equivalents. A transaction function takes a callback that receives a transaction handle, runs one or more Cypher statements against it, and returns a result; the driver commits automatically if the callback returns normally and rolls back if it throws. Critically, the driver also retries the whole callback automatically on a transient error — a deadlock with another transaction, or a leader change during a cluster failover — with no code in the callback aware that a retry happened. Because of that, a transaction-function callback must be free of side effects that should not repeat (do not send an email or call an external API from inside it).

Writing with the Java driver

The following runs entirely against the driver’s Session/transaction-function API — no Spring Data involved — creating a :Person node, a :Movie node, and an ACTED_IN relationship between them inside one retried transaction:

public void addActingCredit(Driver driver, String actorName, int bornYear,
                             String movieTitle, List<String> roles) {
    try (Session session = driver.session(SessionConfig.forDatabase("neo4j"))) {
        session.executeWrite(tx -> {
            tx.run("MERGE (p:Person {name: $name}) ON CREATE SET p.born = $born",
                    Map.of("name", actorName, "born", bornYear));
            tx.run("MERGE (m:Movie {title: $title})",
                    Map.of("title", movieTitle));
            tx.run("""
                    MATCH (p:Person {name: $name}), (m:Movie {title: $title})
                    MERGE (p)-[r:ACTED_IN]->(m)
                    SET r.roles = $roles
                    """,
                    Map.of("name", actorName, "title", movieTitle, "roles", roles));
            return null;
        });
    }
}

MERGE (rather than CREATE) makes the whole callback idempotent, which matters once the driver may run it more than once as part of its own retry. Reference: Java driver: Transactions.

The Python driver’s execute_write

The Python driver mirrors the same session and transaction-function shape, with execute_write in place of executeWrite and a plain function (or lambda) in place of a lambda-typed callback:

# https://neo4j.com/docs/python-manual/current/connect/
from neo4j import GraphDatabase

def add_acting_credit(tx, actor_name, born_year, movie_title, roles):
    tx.run("MERGE (p:Person {name: $name}) ON CREATE SET p.born = $born",
           name=actor_name, born=born_year)
    tx.run("MERGE (m:Movie {title: $title})", title=movie_title)
    tx.run("""
           MATCH (p:Person {name: $name}), (m:Movie {title: $title})
           MERGE (p)-[r:ACTED_IN]->(m)
           SET r.roles = $roles
           """, name=actor_name, title=movie_title, roles=roles)

driver = GraphDatabase.driver("neo4j://localhost:7687", auth=("neo4j", "change-me-please"))
with driver.session(database="neo4j") as session:
    session.execute_write(add_acting_credit, "Hugo Weaving", 1960, "The Matrix", ["Agent Smith"])

The same automatic-retry-on-transient-error behavior applies: execute_write re-invokes add_acting_credit on a deadlock or leader change exactly as executeWrite does in Java. See Python driver: Transactions and Python driver: Connect. The JavaScript, .NET, and Go drivers each expose the equivalent pattern under their own naming convention, all built on the same underlying Bolt transaction semantics.

Where Spring Data Neo4j fits

Spring Data Neo4j does not bypass any of this — its declarative @Transactional support is built directly on top of the driver layer described above. Neo4jTransactionManager opens a Session and drives it through the same transaction machinery a raw executeWrite call would use, and the higher-level Neo4jClient (see that page’s "Custom queries with Neo4jClient" section) also executes through a driver Session underneath. Reaching for the driver’s Session/executeWrite API directly — as on this page — makes sense outside a Spring application, or inside one when a unit of work needs transaction-function retry semantics that @Transactional does not expose directly.

Where to go next

  • Spring Data Neo4j — @Transactional, Neo4jTransactionManager, and Neo4jClient built on top of the driver layer described here.

  • Advanced Cypher querying — the Cypher run inside these transactions, in depth.

  • Java driver: Concurrency — how concurrent transactions and locks interact under read-committed isolation.