Chapter 13 · Application Drivers and Bolt: Sessions, Routing, Parameters, Result Streaming, and Connection Pools
Build a Repository/Service Layer that Keeps Cypher Parameterized, Transactional, Testable, and Measurable
Assemble the driver, repository and service boundaries into a parameterized, retry-safe, testable and observable AtlasMart application layer.
Learning outcomes
The final chapter lab turns the mechanisms into an application boundary: one maintained Driver, a repository that owns Cypher and result transformation, and a service that owns business inputs, idempotency and structured errors. The goal is not a framework-specific architecture; it is to make every database interaction parameterized, transactional, testable and measurable.
Construct one maintained driver with explicit database, pool and retry configuration.
Keep Cypher text and parameters inside repository methods with stable return types.
Keep multi-query business invariants inside managed transactions and external side effects outside retry callbacks.
Attach request/operation metadata and capture result summaries for client/server latency decomposition.
Test repository behavior against the deterministic AtlasMart fixture and failure cases without leaking driver resources.
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. Minimal repository/service ownership model
| Layer | Owns | Must not own |
|---|---|---|
| HTTP/CLI handler | transport validation, request ID, response mapping | raw Cypher string construction |
| Service | business workflow, idempotency key, authorization decision, external side-effect orchestration | connection pool lifecycle per call |
| Repository | parameterized Cypher, transaction boundary, Neo4j result transformation | HTTP response objects or unrelated business side effects |
| Driver bootstrap | URI/auth/TLS/pool/retry configuration and shutdown | request-specific mutable state |
2. Repository implementation
from __future__ import annotationsfrom dataclasses import dataclassfrom neo4j import Driver, Query, unit_of_work@dataclass(frozen=True)class OrderView: order_id: str status: strclass AtlasMartRepository: def __init__(self, driver: Driver, database: str = "neo4j"): self._driver = driver self._database = database def get_order(self, order_id: str, request_id: str) -> OrderView | None: query = Query( """ CYPHER 25 MATCH (o:Order {orderId:$orderId}) RETURN o.orderId AS orderId, o.status AS status """, timeout=3.0, metadata={"app":"atlasmart-api", "request_id":request_id, "operation":"get-order"}, ) with self._driver.session(database=self._database) as session: result = session.run(query, orderId=order_id) record = result.single() summary = result.consume() if record is None: return None return OrderView(record["orderId"], record["status"]) @staticmethod @unit_of_work(timeout=5.0) def _upsert_order_tx(tx, order_id: str, customer_id: str, status: str): return tx.run( """ CYPHER 25 MATCH (c:Customer {customerId:$customerId}) MERGE (o:Order {orderId:$orderId}) ON CREATE SET o.createdAt=datetime() SET o.status=$status MERGE (c)-[:PLACED]->(o) RETURN o.orderId AS orderId, o.status AS status """, orderId=order_id, customerId=customer_id, status=status, ).single(strict=True).data() def upsert_order(self, order_id: str, customer_id: str, status: str): with self._driver.session(database=self._database) as session: return session.execute_write( self._upsert_order_tx, order_id, customer_id, status)
The repository returns Python-owned values, not a live
Result. The managed write can retry, so
orderId is a stable caller-generated identity
backed by the uniqueness constraint established in earlier
chapters.
3. Application bootstrap: one Driver, explicit shutdown
import osfrom neo4j import GraphDatabasefrom atlasmart_repository import AtlasMartRepositoryURI = os.getenv("NEO4J_URI", "bolt://127.0.0.1:7687")AUTH = (os.getenv("NEO4J_USER", "neo4j"), os.getenv("NEO4J_PASSWORD", "atlasmart-course-2026"))driver = GraphDatabase.driver( URI, auth=AUTH, max_connection_pool_size=20, connection_acquisition_timeout=5.0, connection_timeout=5.0, max_connection_lifetime=1800.0, max_transaction_retry_time=15.0,)try: driver.verify_connectivity() repo = AtlasMartRepository(driver, "neo4j") print(repo.get_order("O-5001", "req-local-001")) print(repo.upsert_order("O-SVC-1301", "C-1001", "PAID"))finally: driver.close()
For a real web framework, create this Driver during process/application startup and close it during graceful shutdown. Do not place credentials in source code; the defaults here continue the disposable local lab only.
4. Test deterministic behavior and repeatability
CYPHER 25CREATE CONSTRAINT customer_id IF NOT EXISTSFOR (c:Customer) REQUIRE c.customerId IS UNIQUE;CREATE CONSTRAINT order_id IF NOT EXISTSFOR (o:Order) REQUIRE o.orderId IS UNIQUE;CYPHER 25MERGE (c:Customer {customerId:'C-1001'})SET c.name='Mina Rahimi', c.tier='GOLD'MERGE (p:Product {productId:'P-1001'})SET p.name='Trail Camera', p.category='Cameras', p.price=129.90MERGE (o:Order {orderId:'O-5001'})SET o.status='PAID', o.orderedAt=datetime('2026-09-08T16:30:00Z')MERGE (c)-[:PLACED]->(o)MERGE (o)-[r:CONTAINS]->(p)SET r.quantity=1, r.unitPrice=129.90;
repo = AtlasMartRepository(driver, "neo4j")assert repo.get_order("missing", "test-missing") is Nonefirst = repo.upsert_order("O-SVC-1301", "C-1001", "PAID")second = repo.upsert_order("O-SVC-1301", "C-1001", "PAID")assert first == second == {"orderId":"O-SVC-1301", "status":"PAID"}records, summary, _ = driver.execute_query( """ CYPHER 25 MATCH (:Customer {customerId:'C-1001'})-[r:PLACED]->(o:Order {orderId:'O-SVC-1301'}) RETURN count(o) AS orders, count(r) AS placedEdges """, database_="neo4j")row = records[0]assert row["orders"] == 1assert row["placedEdges"] == 1
Run the same test multiple times. The evidence is not merely “no
exception”; it is the invariant that one business order and one
PLACED edge exist after repeated execution.
5. Measure client/server boundaries
from time import perf_counterrequest_id = "req-measure-1301"t0 = perf_counter()records, summary, _ = driver.execute_query( """ CYPHER 25 MATCH (c:Customer {customerId:$id})-[:PLACED]->(o:Order) RETURN o.orderId AS orderId ORDER BY o.orderedAt DESC, o.orderId """, id="C-1001", database_="neo4j")end_to_end_ms = (perf_counter() - t0) * 1000print({ "request_id": request_id, "records": len(records), "result_available_ms": summary.result_available_after, "result_consumed_ms": summary.result_consumed_after, "client_end_to_end_ms": end_to_end_ms,})
The difference between driver summary timings and end-to-end client time can include pool wait, network transfer, result transformation, application scheduling and serialization. Treat the breakdown as evidence for where to investigate, not as a perfect distributed trace.
6. Failure matrix: prove behavior, not slogans
| Injected condition | Expected class of evidence | Correct response |
|---|---|---|
| wrong password | authentication exception at connectivity/query time | fail startup/request according to secret-rotation policy; do not generic-retry forever |
| pool too small + long-held results | acquisition wait/timeouts and higher client tail latency | reduce connection hold time or capacity-plan pool/DB; do not jump to unbounded pool |
| session shared concurrently | debug/resource/concurrency failure | create separate sessions; keep shared Driver |
| missing order | zero-row application result | return domain not-found; not a database exception |
| retryable transient failure | managed callback may rerun | idempotent transaction and bounded retry budget |
| client timeout after write | commit outcome may be ambiguous | reconcile by stable order/idempotency key |
7. Cleanup/reset
CYPHER 25MATCH (o:Order)WHERE o.orderId IN ['O-DRIVER-1301','O-BOOKMARK-1','O-SVC-1301']DETACH DELETE o;MATCH (r:Reservation {reservationId:'RSV-1301'}) DETACH DELETE r;
Do not detach-delete generic AtlasMart customers/products/orders from earlier chapters. This cleanup is intentionally scoped to IDs created by this chapter.
Production judgment
| Review area | Acceptance evidence |
|---|---|
| API ownership | all query values parameterized; repository owns result transformation; no live cursor leaks |
| pool/timeouts | concurrency test records p50/p95/p99, acquisition failures and server saturation together |
| retry/idempotency | stable keys + uniqueness constraints + repeated-execution invariant tests |
| security | TLS/auth policy matches deployment; secrets externalized; no debug logs expose credentials |
| routing/causality | cluster/Aura tests cover reader/writer placement and only necessary bookmark chains |
| observability | request/operation metadata and client/server timing are correlated |
| upgrade | server 2026.x / driver 6.x compatibility and application Cypher regression tests are part of rollout |
Check your understanding
- Where should the Driver live in a web app?
-
What should a repository return instead of
Result? - Why does repeated upsert testing matter?
-
What does
result_available_afternot include? - What is the bridge to Chapter 14?
Review the answers
1. At application/process scope, created at startup and closed on graceful shutdown.
2. Application-owned records/DTOs/iterators whose lifecycle is explicit and does not depend on a closed session.
3. It proves the write is idempotent under the stable identity contract instead of only succeeding once.
4. It is not the entire HTTP/client request latency; pool wait, network, transformation and serialization can sit outside it.
5. The same driver ownership and resource limits become the foundation for async, concurrent, batch and high-throughput application patterns.
Summary and next step
A reliable Neo4j service owns one long-lived driver, short-lived sessions/managed transactions, parameterized Cypher, bounded result/pool behavior, explicit causal/retry rules and correlated evidence. Chapter 14 extends this foundation into asynchronous, batched and high-throughput workloads.
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.
- Python query advanced mechanisms — Query metadata, timeout and advanced session patterns.
- Python concurrency — Async/concurrent application access; bridge to the next chapter.