How to Handle Transactions with Neo4j in neomodel: Sync, Async, and Best Practices

neomodel provides TransactionProxy and AsyncTransactionProxy classes that wrap Neo4j transactions in Pythonic context managers and decorators, automatically handling commits, rollbacks, bookmarks, and access modes through the db.transaction and adb.transaction APIs.

Handling transactions with Neo4j in neomodel requires understanding the library's proxy-based architecture. The neo4j-contrib/neomodel library abstracts the official Neo4j Python driver's transaction mechanics into high-level Pythonic APIs that support both synchronous and asynchronous workflows. This guide examines the core transaction classes, demonstrates practical usage patterns, and explores advanced features like causal consistency bookmarks and user impersonation.

Understanding neomodel's Transaction Architecture

The transaction system in neomodel centers on proxy objects that implement the context-manager protocol. These proxies live in separate modules for synchronous and asynchronous operations, both exposing a consistent interface to the underlying Neo4j driver.

The TransactionProxy Class (Synchronous)

In neomodel/sync_/transaction.py, the TransactionProxy class implements __enter__ and __exit__ to manage transaction lifecycles. When entering a context, the proxy calls Database.begin() to start a new transaction. On exit, it automatically commits via Database.commit() if no exception occurred, or rolls back via Database.rollback() if an error was raised. The proxy also captures last_bookmarks from the commit result to support causal chaining.

AsyncTransactionProxy for Asynchronous Workflows

The async counterpart lives in neomodel/async_/transaction.py. AsyncTransactionProxy mirrors the sync implementation but uses __aenter__ and __aexit__ to support async with syntax. This allows non-blocking transaction handling in async applications using the same bookmark and rollback semantics as the synchronous version.

Database Integration and Access Modes

The Database class in neomodel/sync_/database.py exposes transaction proxies through properties:

  • Database.transaction – Returns a TransactionProxy with default access mode
  • Database.read_transaction – Returns a proxy configured with ACCESS_MODE_READ
  • Database.write_transaction – Returns a proxy configured with ACCESS_MODE_WRITE

These properties instantiate fresh proxy objects bound to the singleton database instance, allowing fine-grained control over transaction access patterns.

Handling Transactions with Neo4j in neomodel: Core Patterns

Neomodel supports two primary transaction patterns: context managers for block-scoped transactions and decorators for function-scoped transactions. Both patterns automatically handle connection ensuring via the @ensure_connection decorator built into the proxy initialization.

Basic Context Manager Usage (Synchronous)

The most common pattern uses db.transaction as a context manager to wrap database operations:

from neomodel import StructuredNode, StringProperty, db

class Person(StructuredNode):
    name = StringProperty()

# Start a transaction, create a node, and commit automatically

with db.transaction:
    Person(name="Alice").save()

# If an exception occurs inside the block, the transaction rolls back

In neomodel/sync_/transaction.py, the TransactionProxy.__enter__ method calls Database.begin() to initiate the transaction. When the block completes successfully, __exit__ triggers Database.commit() and stores any returned bookmarks. If an exception propagates out of the block, __exit__ invokes Database.rollback() instead.

Read-Only and Write-Only Transactions

For workloads that strictly separate read and write operations, use the specialized proxies:

with db.read_transaction:
    result = Person.nodes.filter(name="Bob").all()

The read_transaction property in neomodel/sync_/database.py (lines 94-96) configures the proxy with access_mode=ACCESS_MODE_READ, instructing the Neo4j driver to route the transaction to read replicas in a cluster topology. Similarly, write_transaction forces write routing.

Function Decorator Pattern

Decorate functions to execute their entire body within a transaction:

@db.transaction
def create_two_people():
    Person(name="Carol").save()
    Person(name="Dave").save()

create_two_people()   # All statements run inside a single transaction

In neomodel/sync_/transaction.py, the TransactionProxy.__call__ method returns a wrapper function that executes the decorated callable inside a with self: block. This ensures atomicity across multiple database operations without explicit context manager syntax in the business logic.

Advanced Transaction Management

Beyond basic commit and rollback, neomodel provides mechanisms for causal consistency, user impersonation, and specific error handling.

Causal Consistency with Bookmarks

Neo4j bookmarks guarantee causal chaining between transactions. The with_bookmark decorator extends the standard transaction decorator to accept and return bookmarks:

@db.transaction.with_bookmark
def create_person(name, **kwargs):
    # Accept an optional 'bookmarks' kwarg

    Person(name=name).save()
    # The decorator returns (result, last_bookmarks)

    return None

# First call – no bookmark needed

_, bm = create_person(name="Eve")

# Subsequent call – pass the previous bookmark to guarantee ordering

_, bm2 = create_person(name="Frank", bookmarks=bm)

In neomodel/sync_/transaction.py (lines 70-89), the with_bookmark implementation stores incoming bookmarks in self.bookmarks before entering the transaction context. After successful commit, it returns a tuple containing the function result and the last_bookmarks captured from the commit response.

User Impersonation (Enterprise Edition)

For applications requiring elevated privileges or audit trails, the Database.impersonate() context manager temporarily switches the Neo4j user context:

with db.impersonate("alice"):
    # All queries inside run as the Neo4j user "alice"

    Person(name="Impersonated").save()

Implemented in neomodel/sync_/database.py (lines 22-38), the ImpersonationHandler stores the original user context, sets Database.impersonated_user to the target username, and restores the original context on exit. This feature requires Neo4j Enterprise Edition.

Error Handling and Rollback Behavior

The transaction proxy automatically translates certain Neo4j errors into domain-specific exceptions. In neomodel/sync_/transaction.py (lines 50-55), the __exit__ method catches schema constraint violations and re-raises them as UniqueProperty exceptions, providing clearer semantics for property uniqueness violations.

If any exception propagates out of the transaction block, the __exit__ method ensures Database.rollback() is called, maintaining atomicity. Successful completion triggers Database.commit() and captures last_bookmarks for subsequent causal chaining.

Asynchronous Transaction Handling

Modern Python applications using asyncio can leverage the async database instance adb with identical transaction semantics.

Async Context Managers

Use async with to manage transactions in coroutines:

import asyncio
from neomodel import StructuredNode, StringProperty, adb   # async DB instance

class Article(StructuredNode):
    title = StringProperty()

async def main():
    async with adb.transaction:
        await Article(title="Async intro").save()

asyncio.run(main())

In neomodel/async_/transaction.py, the AsyncTransactionProxy implements __aenter__ and __aexit__ to mirror the synchronous context manager protocol. This ensures that async applications receive the same automatic commit, rollback, and bookmark handling as sync code.

Summary

  • neomodel abstracts Neo4j transactions through TransactionProxy (sync) and AsyncTransactionProxy (async) classes located in neomodel/sync_/transaction.py and neomodel/async_/transaction.py.
  • Use with db.transaction: for basic atomic blocks, with db.read_transaction: for read-only operations, and @db.transaction for function-level atomicity.
  • Implement causal consistency across distributed systems using @db.transaction.with_bookmark to pass and receive bookmarks between transactions.
  • Leverage with db.impersonate("username"): (Enterprise Edition) to execute queries under different Neo4j user contexts.
  • Async applications use async with adb.transaction: with identical semantics to the synchronous API.

Frequently Asked Questions

How do I ensure a series of database operations are atomic in neomodel?

Wrap the operations in a with db.transaction: block or decorate the function with @db.transaction. If any exception occurs during execution, TransactionProxy.__exit__ automatically calls Database.rollback() to revert changes; otherwise, it commits via Database.commit().

What is the difference between db.transaction and db.read_transaction?

The db.transaction property returns a TransactionProxy with default access mode, while db.read_transaction configures the proxy with ACCESS_MODE_READ. Using read_transaction ensures Neo4j routes the query to read replicas in cluster deployments, optimizing read performance and write safety.

How do I maintain causal consistency across multiple transactions?

Use the @db.transaction.with_bookmark decorator on functions that need to participate in causal chains. Pass the bookmark from a previous transaction via the bookmarks keyword argument; the decorator returns a tuple (result, last_bookmarks) containing the new bookmark to pass to subsequent operations.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →