Chapter 13 · Application Drivers and Bolt: Sessions, Routing, Parameters, Result Streaming, and Connection Pools
Read/Write Routing, Bookmarks, Retry Logic, Error Classification, and Observability Correlation
Route and retry deliberately, use bookmarks only for required causal dependencies, and correlate client requests with database work without overclaiming consistency.
Learning outcomes
AtlasMart writes an order and immediately serves a read from another service instance. In a cluster, routing and replication mean “which server answered?” matters. The driver can route reads/writes and carry causal bookmarks, but bookmarks are not a blanket serializability guarantee and indiscriminate retries can duplicate external side effects.
Use routing-enabled URI/access modes without treating read mode as authorization.
Explain routing-table refresh and the direct-member bypass
created by bolt://.
Use bookmarks to establish causal read-after-write dependencies across sessions where required.
Let managed transactions retry only retryable database work and keep callbacks idempotent.
Correlate client request IDs, transaction metadata, result summaries and server observations.
The mandatory lab continues Neo4j Community
2026.07.1, database neo4j, explicit
CYPHER 25 where query-language version matters,
container atlasmart-neo4j, loopback Bolt
127.0.0.1:7687, authentication
neo4j/atlasmart-course-2026, and no
TLS on the loopback-only disposable instance. The current
official Python driver is neo4j 6.3.0 (Python
3.10–3.14), whose API supports Bolt 6.0–6.1, Bolt 5.0–5.8 and
Bolt 4.4. Neo4j 5.26.30 remains the LTS
comparison line.
This generation environment does not connect to the AtlasMart Neo4j container, so no handshake, routing-table, pool, TLS, bookmark, latency or retry output is fabricated. Each exercise gives commands and invariants to capture on your machine. True multi-member read/write routing requires an Enterprise cluster or Aura deployment; the mandatory Community path proves the driver/session/pool/stream semantics locally and labels cluster-only observations separately.
1. Routing is placement, not security
| Client choice | Routing intent | Important boundary |
|---|---|---|
RoutingControl.READ /
execute_read()
|
route read work to eligible readers in a cluster | the driver does not parse Cypher for authorization; read mode is not a security control |
RoutingControl.WRITE /
execute_write()
|
route write work to an eligible primary/writer |
default for execute_query() unless
overridden
|
neo4j:// |
obtain/refresh routing table | requires advertised/discoverable addresses appropriate to deployment |
bolt:// |
direct connection to named member | bypasses routing table; caller owns availability/role implications |
2. Parameterized read/write with explicit routing intent
records, write_summary, _ = store.driver.execute_query( """ CYPHER 25 MERGE (o:Order {orderId:$orderId}) ON CREATE SET o.createdAt=datetime() SET o.status=$status RETURN o.orderId AS orderId """, orderId="O-DRIVER-1301", status="PAID", database_="neo4j", routing_=RoutingControl.WRITE,)records, read_summary, _ = store.driver.execute_query( """ CYPHER 25 MATCH (o:Order {orderId:$orderId}) RETURN o.status AS status """, orderId="O-DRIVER-1301", database_="neo4j", routing_=RoutingControl.READ,)
execute_query() uses a driver bookmark manager by
default, so sequential calls on that driver can be causally
chained. In high-throughput systems, evaluate the latency cost
of bookmark coordination and only require cross-session causal
waiting where the business dependency needs it.
3. Bookmarks express “after this state,” not global serializability
with store.driver.session(database="neo4j") as writer: writer.execute_write(lambda tx: tx.run( "MERGE (o:Order {orderId:$id}) SET o.status='PAID'", id="O-BOOKMARK-1").consume()) bookmark = writer.last_bookmarks()with store.driver.session(database="neo4j", bookmarks=bookmark) as reader: status = reader.execute_read(lambda tx: tx.run( "MATCH (o:Order {orderId:$id}) RETURN o.status AS status", id="O-BOOKMARK-1").single()["status"]) print(status)
The reader will not execute before the bookmark state is established on the chosen server. This is a causal dependency. It does not order unrelated clients globally, eliminate transaction races, or replace uniqueness/locking/invariant design.
4. Retry only work the driver classifies as retryable
execute_read()/execute_write() managed
transaction callbacks may be executed more than once when a
retryable failure occurs, until the retry budget is exhausted.
The callback therefore needs idempotent database behavior and
must not send an email, charge a card or mutate
non-transactional global state on each attempt.
from neo4j import unit_of_work@unit_of_work(timeout=5.0, metadata={"app":"atlasmart-api","operation":"reserve"})def reserve(tx, reservation_id, product_id): record = tx.run( """ CYPHER 25 MERGE (r:Reservation {reservationId:$reservationId}) ON CREATE SET r.createdAt=datetime() WITH r MATCH (p:Product {productId:$productId}) MERGE (r)-[:RESERVES]->(p) RETURN r.reservationId AS id """, reservationId=reservation_id, productId=product_id, ).single(strict=True) return record["id"]with store.driver.session(database="neo4j") as session: reservation_id = session.execute_write( reserve, "RSV-1301", "P-1001")
Generate the idempotency key RSV-1301 outside the
callback and enforce its uniqueness. Perform external side
effects after the database operation through an
outbox/reconciliation design, not directly inside a retryable
callback.
5. Error classification and ambiguous outcomes
from neo4j.exceptions import Neo4jError, ServiceUnavailable, AuthErrortry: do_database_work()except AuthError: raise # credentials/configuration: do not blind-retryexcept Neo4jError as exc: if exc.is_retryable(): # explicit transactions may choose a bounded retry policy. # managed transactions already implement retries. raise raiseexcept ServiceUnavailable: # map to your availability policy; outcome may need reconciliation raise
A client timeout or connection loss does not prove a transaction failed to commit. For business-critical writes, use idempotency keys and reconciliation queries so a retry can discover/confirm the already-committed state rather than duplicating it.
6. Correlate client and server work
from neo4j import Queryrequest_id = "req-20260909-00042"query = Query( "CYPHER 25 MATCH (o:Order {orderId:$id}) RETURN o.status AS status", timeout=3.0, metadata={"app":"atlasmart-api", "request_id":request_id, "operation":"order-read"},)with store.driver.session(database="neo4j") as session: result = session.run(query, id="O-5001") record = result.single(strict=True) summary = result.consume() print(request_id, record["status"], summary.result_available_after, summary.result_consumed_after)
SHOW TRANSACTIONS YIELD transactionId, currentQuery, metaData, elapsedTime, statusWHERE metaData.request_id = 'req-20260909-00042'RETURN transactionId, currentQuery, metaData, elapsedTime, status;
SHOW TRANSACTIONS visibility/privileges differ by
edition and user. Community local admin can use it for learning,
while production access should follow least privilege.
Production judgment
| Question | Safe interpretation |
|---|---|
| Should every read use a bookmark? | No. Use causal coordination when a read must observe a particular prior write; extra waiting can cost latency. |
| Can READ routing stop writes? | No. Access mode is routing intent, not authorization. |
| Can every exception be retried? | No. Retry only documented retryable/transient conditions, within a budget, and only for idempotent work. |
| Does timeout mean rollback? | No. Outcome can be ambiguous after network/client timeout; reconcile by stable business identity. |
| What makes routing observable? | Driver pool/debug logs for diagnosis plus server/request metadata; true multi-role behavior needs a cluster/Aura environment. |
Check your understanding
- What does a bookmark guarantee?
- Why should a managed transaction callback avoid sending an email?
-
Why use
neo4j://instead ofbolt://for a cluster? - What should you do after an ambiguous timeout on an idempotent order write?
- Are driver debug logs a stable telemetry API?
Review the answers
1. A later transaction can wait until the represented database state is established, supporting causal ordering; it is not global serializability.
2. The callback may be executed more than once during retries, so a non-transactional side effect can duplicate.
3. It enables routing discovery and role-aware placement instead of pinning the named address.
4. Query by the stable order/idempotency key and reconcile the committed state before deciding to repeat.
5. No. Their exact format is not an API contract; use them for diagnosis, not production parsing.
Summary and next step
Routing, bookmarks and retries are reliable only when repository methods own stable identities, transaction boundaries and observability. Lesson 5 assembles those parts into a small AtlasMart service layer.
Authoritative references
- Current Neo4j versions — Current server and 5.26 LTS release snapshot.
- Neo4j Python Driver Manual — Official application-driver guide used by the mandatory lab.
- Python Driver 6.3 API — Current API, Bolt compatibility and lifecycle contract.
- Driver connection guide — Driver lifetime, connectivity checks and cluster routing.
- Advanced connection information — URI schemes, TLS, resolver and connection configuration.
- Transactions with the Python driver — Session/transaction lifecycle, managed retries and result streaming.
- Python driver performance recommendations — Lazy streaming, fetch size and read routing guidance.
- Bolt compatibility matrix — Neo4j DBMS and negotiated Bolt protocol versions.
- Bookmarks — Causal chaining within/across sessions and performance considerations.
- Driver logging API — Debug logger hierarchy and non-contractual message format.