Skill v1.0.1
currentAutomated scan100/100+4 new
name: neo4j-driver-java-skill description: Neo4j Java Driver v6 — driver lifecycle, Maven/Gradle setup, executableQuery, executeRead/Write managed transactions, explicit transactions, async/reactive patterns, error handling, data type mapping, connection pool tuning, causal consistency/bookmarks. Use when writing Java or Kotlin code that connects to Neo4j via GraphDatabase.driver, executableQuery, SessionConfig, executeRead, executeWrite, or TransactionCallback. Does NOT handle Cypher authoring — use neo4j-cypher-skill. Does NOT cover driver version upgrades — use neo4j-migration-skill. Does NOT cover Spring Data Neo4j (@Node, Neo4jRepository) — use neo4j-spring-data-skill. version: 1.0.1 allowed-tools: Bash WebFetch
When to Use
- Java/Kotlin code connecting to Neo4j (Aura or self-managed)
- Setting up driver, sessions, transactions in Maven/Gradle projects
- Debugging result handling, error recovery, connection pool issues
- Async (
CompletableFuture) or reactive (Project Reactor / RxJava) Neo4j access
When NOT to Use
- Cypher query authoring/optimization →
neo4j-cypher-skill - Driver version upgrades →
neo4j-migration-skill - Spring Data Neo4j (
@Node,@Relationship,Neo4jRepository) →neo4j-spring-data-skill
Dependency
Maven
<dependency><groupId>org.neo4j.driver</groupId><artifactId>neo4j-java-driver</artifactId><version>6.1.0</version></dependency>
Gradle
implementation 'org.neo4j.driver:neo4j-java-driver:6.1.0'
Check latest: https://central.sonatype.com/artifact/org.neo4j.driver/neo4j-java-driver
Environment Variables
Standard pattern for connection config — never hardcode credentials:
String uri = System.getenv().getOrDefault("NEO4J_URI", "neo4j://localhost:7687");String user = System.getenv().getOrDefault("NEO4J_USERNAME", "neo4j");String password = System.getenv().getOrDefault("NEO4J_PASSWORD", "");String database = System.getenv().getOrDefault("NEO4J_DATABASE", "neo4j");
Spring Boot: inject via @Value("${spring.neo4j.uri}") or application.properties:
spring.neo4j.uri=neo4j+s://xxx.databases.neo4j.iospring.neo4j.authentication.username=neo4jspring.neo4j.authentication.password=secret
Driver Lifecycle
One Driver per application — thread-safe, expensive to create. Implement AutoCloseable or use try-with-resources.
// Long-lived singletonvar driver = GraphDatabase.driver("neo4j+s://xxx.databases.neo4j.io", // Aura TLS+routingAuthTokens.basic(user, password));driver.verifyConnectivity(); // fail fast// Short-lived (tests / CLI)try (var driver = GraphDatabase.driver(uri, AuthTokens.basic(user, password))) {driver.verifyConnectivity();// ...}
URI schemes:
| URI | Use | |
|---|---|---|
neo4j://localhost | Unencrypted, cluster routing | |
neo4j+s://xxx.databases.neo4j.io | TLS + cluster routing (Aura) | |
bolt://localhost:7687 | Unencrypted, single instance | |
bolt+s://localhost:7687 | TLS, single instance |
Auth options: AuthTokens.basic(u,p) · AuthTokens.bearer(token) · AuthTokens.kerberos(b64) · AuthTokens.none()
Choosing the Right API
| API | When | Auto-retry | Streaming | |
|---|---|---|---|---|
driver.executableQuery() | Default for most queries | ✅ | ❌ eager | |
session.executeRead/Write() | Large results, callback control | ✅ | ✅ | |
session.beginTransaction() | Multi-method, external coordination | ❌ | ✅ | |
session.run() | Self-managing queries (CALL IN TRANSACTIONS) | ⚠️ one-shot [6.1+] | ✅ | |
driver.asyncSession() | Non-blocking CompletableFuture | ✅ | ✅ | |
driver.rxSession() | Reactor/RxJava backpressure | ✅ | ✅ |
CALL { … } IN TRANSACTIONS and USING PERIODIC COMMIT self-manage their transaction — use session.run() only. executableQuery and executeRead/Write will fail for these queries.
session.run() retry [6.1+]: single immediate retry on idempotent errors only (enabled by default). Disable per driver or per session:
// Driver-level — disable for all sessionsvar config = Config.builder().withAutoCommitRetriesDisabled(true).build();// Session-level — overrides drivervar sessionConfig = SessionConfig.builder().withAutoCommitRetriesMode(AutoCommitRetriesMode.DISABLED) // DEFAULT = follow driver.build();
executableQuery — Default
// Read — route to replicasvar result = driver.executableQuery("""MATCH (p:Person {name: $name})-[:KNOWS]->(friend)RETURN friend.name AS name""").withParameters(Map.of("name", "Alice")).withConfig(QueryConfig.builder().withDatabase("neo4j") // always specify — avoids home-db round-trip.withRouting(RoutingControl.READ).build()).execute();result.records().forEach(r -> System.out.println(r.get("name").asString()));long ms = result.summary().resultAvailableAfter(TimeUnit.MILLISECONDS);// Writedriver.executableQuery("CREATE (p:Person {name: $name, age: $age})").withParameters(Map.of("name", "Bob", "age", 30)).withConfig(QueryConfig.builder().withDatabase("neo4j").build()).execute();
Never string-interpolate Cypher. Always .withParameters(Map.of(...)).
Managed Transactions (executeRead / executeWrite)
Sessions are NOT thread-safe — one per request/thread, always close.
try (var session = driver.session(SessionConfig.builder().withDatabase("neo4j").build())) {// Read → replica routingvar names = session.executeRead(tx -> {var result = tx.run("MATCH (p:Person) WHERE p.name STARTS WITH $prefix RETURN p.name AS name",Map.of("prefix", "Al"));return result.stream().map(r -> r.get("name").asString()).toList(); // collect INSIDE});// Write → leader routingsession.executeWriteWithoutResult(tx ->tx.run("CREATE (p:Person {name: $name})", Map.of("name", "Carol")));}
Result must be consumed INSIDE the callback
Result is a lazy cursor tied to the open transaction. Transaction closes when callback returns — any read after that throws ResultConsumedException.
// ❌ Returns Result — already closed by the time caller uses itvar result = session.executeRead(tx ->tx.run("MATCH (p:Person) RETURN p.name AS name"));result.stream().forEach(...); // throws ResultConsumedException// ✅ Collect to List inside callbackvar names = session.executeRead(tx ->tx.run("MATCH (p:Person) RETURN p.name AS name").stream().map(r -> r.get("name").asString()).toList());
Callback rules
- Consume each
Resultbefore nexttx.run()— multiple open cursors = undefined behaviour. - No side effects (HTTP, email, metric increments) — callback may be retried on transient errors.
- Use
MERGE(idempotent), notCREATE, for retry-safe writes. executeRead→ replica;executeWrite→ leader.
TransactionConfig — timeouts & metadata
var config = TransactionConfig.builder().withTimeout(Duration.ofSeconds(5)).withMetadata(Map.of("app", "myService", "user", userId)) // visible in SHOW TRANSACTIONS.build();session.executeRead(tx -> { /* ... */ }, config);
Explicit Transactions
Use when work spans multiple methods or requires external coordination. Not auto-retried.
try (var session = driver.session(SessionConfig.builder().withDatabase("neo4j").build())) {var tx = session.beginTransaction();try {doPartA(tx);doPartB(tx);tx.commit();} catch (Exception e) {try { tx.rollback(); } catch (Exception rb) { e.addSuppressed(rb); }throw e;}}
tx.rollback() is a network call — wrap in its own try/catch and use addSuppressed so the original exception is not lost.
Commit uncertainty: if tx.commit() throws ServiceUnavailableException, the commit may or may not have succeeded. Design writes as idempotent (MERGE + unique constraints) so retrying is safe.
Choose explicit vs managed:
- Auto-retry needed →
executeRead/executeWrite - Work spans multiple methods → explicit (pass
txas parameter) - Coordinating with external I/O → explicit (commit only after I/O succeeds)
Error Handling
try {driver.executableQuery("...").execute();} catch (ServiceUnavailableException e) {// No servers — check connection} catch (SessionExpiredException e) {// Server closed session — open new one} catch (TransientException e) {// Managed txns retry automatically; explicit txns need manual retry} catch (Neo4jException e) {// Cypher/constraint error — e.code() gives GQL status code}
Managed transactions auto-retry TransientException — no catch needed.
Data Types & Value Extraction
| Cypher type | Java accessor | |
|---|---|---|
Integer | value.asLong() / value.asInt() | |
Float | value.asDouble() | |
String | value.asString() | |
Boolean | value.asBoolean() | |
List | value.asList() | |
Map | value.asMap() | |
Node | value.asNode() | |
Relationship | value.asRelationship() | |
Date | value.asLocalDate() | |
DateTime | value.asZonedDateTime() |
var record = result.records().get(0);String name = record.get("name").asString();long age = record.get("age").asLong();var node = record.get("p").asNode();String label = node.labels().iterator().next();Map<String,Object> props = node.asMap();
Null safety — two distinct cases
| Situation | record.get(key) | .asString() | |
|---|---|---|---|
| Key present, value non-null | the value | returns string | |
| Key present, value is graph null | Value where .isNull() = true | throws Uncoercible | |
| Key absent (typo / not projected) | Value.NULL sentinel | throws NoSuchElementException |
// Graph null — use default overload (safe only if key is always projected):String city = record.get("city").asString("Unknown");// Absent key — check containsKey first:if (record.containsKey("city") && !record.get("city").isNull()) {String city = record.get("city").asString();}
Object Mapping
Map query results to Java records/classes directly — eliminates manual accessor calls.
// Domain record — field names match RETURN aliases (case-sensitive)public record Person(String name, long age) {}// Map single recordvar person = driver.executableQuery("MATCH (p:Person {name: $name}) RETURN p.name AS name, p.age AS age").withParameters(Map.of("name", "Alice")).withConfig(QueryConfig.builder().withDatabase("neo4j").build()).execute().records().stream().map(r -> r.get("name").asString()) // or: r.as(Person.class) — see note.findFirst().orElseThrow();// Using .as(Person.class) — maps RETURN keys to record fields by namevar person2 = driver.executableQuery("""MATCH (p:Person {name: $name})RETURN p.name AS name, p.age AS age""").withParameters(Map.of("name", "Tom Hanks")).withConfig(QueryConfig.builder().withDatabase("neo4j").build()).execute().records().stream().map(record -> record.get("p").as(Person.class)).findFirst().orElseThrow(() -> new RuntimeException("Person not found"));
Nested mapping — return a map projection and include COLLECT {} for lists:
public record Movie(String title, List<Person> actors) {}var movieCypher = """MATCH (movie:Movie)LIMIT 1RETURN movie {.title,actors: COLLECT {MATCH (actor:Person)-[:ACTED_IN]->(movie)RETURN actor}}""";var movie = driver.executableQuery(movieCypher).withConfig(QueryConfig.builder().withDatabase("neo4j").build()).execute().records().stream().map(r -> r.get("movie").as(Movie.class)).findFirst().orElseThrow();
Only mapped properties defined in the record are populated — extra properties returned by Cypher are ignored.
Performance Patterns
Always specify database — omitting triggers home-db round-trip on every call.
Route reads to replicas — RoutingControl.READ in QueryConfig or use executeRead.
Batch writes with `UNWIND` — pass List<Map<String,Object>> (plain maps only; custom objects fail):
List<Map<String, Object>> rows = people.stream().map(p -> Map.<String, Object>of("name", p.name(), "age", p.age())).toList();driver.executableQuery("UNWIND $items AS item MERGE (p:Person {name: item.name}) SET p.age = item.age").withParameters(Map.of("items", rows)).withConfig(QueryConfig.builder().withDatabase("neo4j").build()).execute();
Allowed leaf types in parameter maps: String, Long/Integer/Short/Byte, Double/Float, Boolean, List<?>, Map<String,?>, null. Custom objects and LocalDate must be converted first.
Group writes in one transaction — one executeWrite with a loop, not one executeWrite per iteration.
Connection pool — default 100 connections. Tune if exhausted:
Config.builder().withMaxConnectionPoolSize(50).withConnectionAcquisitionTimeout(30, TimeUnit.SECONDS).build()
Common Errors
| Mistake | Fix | |
|---|---|---|
| String-interpolate Cypher params | .withParameters(Map.of(...)) always | |
| Omit database name | Set in QueryConfig / SessionConfig every time | |
New Driver per request | Create once at startup; share everywhere | |
Share Session across threads | One session per request/thread | |
Return Result from tx callback | Collect to List/Map inside callback | |
Leave Result open before next tx.run() | Consume before next call | |
| Side effects in managed tx callback | Move outside — callback may retry | |
| Pass custom objects to UNWIND params | Convert to List<Map<String,Object>> | |
asString() on graph null | .asString("default") or check .isNull() | |
asString() on absent key | containsKey() before optional access | |
Naked tx.rollback() in catch | Wrap in try/catch; use addSuppressed | |
Assume commit() failure = no commit | Commit uncertainty — design writes idempotent | |
Block inside async callback (.join()) | Chain with thenCompose | |
| Skip session close in async error path | exceptionallyCompose to close then re-throw | |
| One transaction per write in loop | Batch with UNWIND or group in one callback | |
executeWrite for a read | Use executeRead — routes to replica |
References
Load on demand:
- references/async-reactive.md — full async
CompletableFuturepatterns, reactiveRxSessionwithFlux.usingWhen, deadlock avoidance - references/advanced-config.md — full
Config.builder()options, TLS, notification filtering, session-level auth, user impersonation, cross-session bookmarks, spatial types (Values.point/WGS-84/Cartesian)
Docs:
- Java Driver manual: https://neo4j.com/docs/java-manual/current/
- API reference: https://neo4j.com/docs/api/java-driver/current/
Checklist
- [ ] One
Driverinstance created at startup; closed on shutdown - [ ]
verifyConnectivity()called after driver creation - [ ] Database name specified in every
QueryConfig/SessionConfig - [ ] Parameters used (never string-interpolated Cypher)
- [ ]
Resultconsumed inside managed transaction callback - [ ] No side effects inside
executeRead/Writecallbacks - [ ] Sessions closed via try-with-resources
- [ ] Async sessions closed in both success and error paths (
exceptionallyCompose) - [ ]
ServiceUnavailableExceptionon commit handled as commit-uncertain - [ ]
UNWINDparams areList<Map<String,Object>>(no custom objects) - [ ]
containsKey()checked before accessing optional result columns