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

> Master Neo4j transactions in neomodel. Learn to handle sync and async operations with TransactionProxy and context managers for seamless commits and rollbacks.

- Repository: [Neo4j Contrib/neomodel](https://github.com/neo4j-contrib/neomodel)
- Tags: how-to-guide
- Published: 2026-03-08

---

**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`](https://github.com/neo4j-contrib/neomodel/blob/main/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`](https://github.com/neo4j-contrib/neomodel/blob/main/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`](https://github.com/neo4j-contrib/neomodel/blob/main/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:

```python
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`](https://github.com/neo4j-contrib/neomodel/blob/main/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:

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

```

The `read_transaction` property in [`neomodel/sync_/database.py`](https://github.com/neo4j-contrib/neomodel/blob/main/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:

```python
@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`](https://github.com/neo4j-contrib/neomodel/blob/main/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:

```python
@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`](https://github.com/neo4j-contrib/neomodel/blob/main/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:

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

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

```

Implemented in [`neomodel/sync_/database.py`](https://github.com/neo4j-contrib/neomodel/blob/main/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`](https://github.com/neo4j-contrib/neomodel/blob/main/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:

```python
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`](https://github.com/neo4j-contrib/neomodel/blob/main/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`](https://github.com/neo4j-contrib/neomodel/blob/main/neomodel/sync_/transaction.py) and [`neomodel/async_/transaction.py`](https://github.com/neo4j-contrib/neomodel/blob/main/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.