Neo4j
is a graph database management system developed by Neo4j, Inc. Neo4j is a native graph database focused not only on the data itself, but especially on the relations between data.
Neo4j stores data as a property graph, which consists of vertices or nodes as we call them, connected with edges or relationships.
Both of them can have properties.
Neo4j offers Cypher, a declarative query language much like SQL.
Cypher is used to for both querying the graph and creating or updating nodes and relationships.
As a declarative language it used to tell the database what to do and not how to do it.
Learn more about Cypher in the
Neo4j Cypher manual
.
Cypher is not only available in Neo4j, but for example coming to
Apache Spark
.
A spec called
OpenCypher
is available, too.
Neo4j - as the most popular graph database according to DB-Engines ranking - provides a variety of drivers for various languages.
The Quarkus Neo4j extension is based on the official
Neo4j Java Driver
.
The extension provides an instance of the driver configured ready for usage in any Quarkus application.
You will be able to issue arbitrary Cypher statements over Bolt with this extension.
Those statements can be simple CRUD statements as well as complex queries, calling graph algorithms and more.
The driver itself is released under the Apache 2.0 license,
while Neo4j itself is available in a GPL3-licensed open-source "community edition",
with online backup and high availability extensions licensed under a closed-source commercial license.
The reactive programming model is only available when connected against a 4.0+ version of Neo4j.
Reactive programming in Neo4j is fully end-to-end reactive and therefore requires a server that supports backpressure.
In this guide you will learn how to
This guide will focus on asynchronous access to Neo4j, as this is ready to use for everyone.
At the end of this guide, there will be a reactive version, which needs however a 4.0 database version.
This starts a Neo4j instance, that publishes its Bolt port on
7687
and a web interface on
http://localhost:7474
.
Have a look at the
download page
for other options to get started with the product itself.
We recommend that you follow the instructions in the next sections and create the application step by step.
However, you can go right to the completed example.
Clone the Git repository: git clone
https://github.com/quarkusio/quarkus-quickstarts.git
, or download an archive.
The solution is located in the
neo4j-quickstart
directory
.
It contains a very simple UI to use the JAX-RS resources created here, too.
mvn io.quarkus.platform:quarkus-maven-plugin:3.15.0:create \
-DprojectGroupId=org.acme \
-DprojectArtifactId=neo4j-quickstart \
-DclassName="org.acme.datasource.GreetingResource" \
-Dextensions="neo4j,resteasy-reactive-jackson"
cd neo4j-quickstart
It generates:
The Neo4j extension has been added already to your
pom.xml
.
In addition, we added
resteasy-reactive-jackson
, which allows us to expose
Fruit
instances over HTTP in the JSON format via JAX-RS resources.
If you have an already created project, the
neo4j
extension can be added to an existing Quarkus project with the
add-extension
command:
./mvnw quarkus:add-extension -Dextensions="neo4j"
Otherwise, you can manually add this to the dependencies section of your
pom.xml
file:
<dependency>
<groupId>io.quarkiverse.neo4j</groupId>
<artifactId>quarkus-neo4j</artifactId>
<version>4.4.0</version>
</dependency>
src/main/resources/application.properties
# Those are the default values and are implicitly assumed
quarkus.neo4j.uri = bolt://localhost:7687
quarkus.neo4j.authentication.username = neo4j
quarkus.neo4j.authentication.password = secret
You’ll recognize the authentication here that you passed on to the docker command above.
Having done that, the driver is ready to use, there are however other configuration options, detailed below.
Quarkus supports a feature called Dev Services that allows you to create various datasources without any config.
In the case of Neo4j this support applies to the single Neo4j driver instance.
Dev Services will bring up a Neo4j container if you didn’t explicit add the default values or configured custom values for
any of
quarkus.neo4j.uri
,
quarkus.neo4j.authentication.username
or
quarkus.neo4j.authentication.password
.
If Neo4j seems to be reachable via the default properties, Dev Services will also step back.
Otherwise, Quarkus will automatically start a Neo4j container when running tests or dev-mode,
and automatically configure the connection.
When running the production version of the application, the Neo4j connection need to be configured as normal,
so if you want to include a production database config in your
application.properties
and continue to use Dev Services
we recommend that you use the
%prod.
profile to define your Neo4j settings.
Configuration property fixed at build time - All other configuration properties are overridable at runtime
If DevServices has been explicitly enabled or disabled. DevServices is generally enabled by default, unless there is an existing configuration present. When DevServices is enabled Quarkus will attempt to automatically configure and start a database when running in Dev or Test mode.
Environment variable:
QUARKUS_NEO4J_DEVSERVICES_ENABLED
boolean
This value can be used to specify the port to which the bolt-port of the container is exposed. It must be a free port, otherwise startup will fail. A random, free port will be used by default. Either way, a messsage will be logged on which port the Neo4j container is reachable over bolt.
Environment variable:
QUARKUS_NEO4J_DEVSERVICES_BOLT_PORT
This value can be used to specify the port to which the http-port of the container is exposed. It must be a free port, otherwise startup will fail. A random, free port will be used by default. Either way, a messsage will be logged on which port the Neo4j Browser is available.
Environment variable:
QUARKUS_NEO4J_DEVSERVICES_HTTP_PORT
The result of a statement consists usually of one or more
org.neo4j.driver.Record
.
Those records contain arbitrary values, supported by the driver.
If you return a node of the graph, it will be a
org.neo4j.driver.types.Node
.
We add the following method to the
Fruit
, as a convenient way to create them:
public static Fruit from(Node node) {
return new Fruit(UUID.fromString(node.get("id").asString()), node.get("name").asString());
Add a FruitResource
skeleton like this and @Inject
a org.neo4j.driver.Driver
instance and a ThreadContext
instance:
src/main/java/org/acme/neo4j/FruitResource.java
package org.acme.neo4j;
import java.net.URI;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletionException;
import java.util.concurrent.CompletionStage;
import jakarta.inject.Inject;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.DELETE;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.Response.ResponseBuilder;
import jakarta.ws.rs.core.Response.Status;
import org.eclipse.microprofile.context.ThreadContext;
import org.neo4j.driver.Driver;
import org.neo4j.driver.async.AsyncSession;
import org.neo4j.driver.async.ResultCursor;
import org.neo4j.driver.exceptions.NoSuchRecordException;
@Path("/fruits")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public class FruitResource {
@Inject
Driver driver;
@Inject
ThreadContext threadContext; (1)
The ThreadContext
is related to context propagation with completion stage. A completion stage,
unlike Mutiny, does not have a hook to automatically "capture and restore" the context.
So, we need to use this construct in later steps when using the connections asynchronous api.
public CompletionStage<Response> get() {
AsyncSession session = driver.session(AsyncSession.class); (1)
CompletionStage<List<Fruit>> cs = session
.executeReadAsync(tx -> tx
.runAsync("MATCH (f:Fruit) RETURN f ORDER BY f.name") (2)
.thenCompose(cursor -> cursor (3)
.listAsync(record -> Fruit.from(record.get("f").asNode()))));
return threadContext.withContextCapture(cs) (4)
.thenCompose(fruits -> (5)
session.closeAsync().thenApply(signal -> fruits))
.thenApply(Response::ok) (6)
.thenApply(ResponseBuilder::build);
Retrieve a cursor, list the results and create Fruit
s. This must happen inside the transactional function, not outside.
Wrap the completion stage so that the current context is captured, and restored it before calling a continuation
method of the completion stage. With that, the context is restored and available in any callback.
Close the session after processing
Create a JAX-RS response
@POST
public CompletionStage<Response> create(Fruit fruit) {
AsyncSession session = driver.session(AsyncSession.class);
CompletionStage<Fruit> cs = session
.executeWriteAsync(tx -> tx
.runAsync(
"CREATE (f:Fruit {id: randomUUID(), name: $name}) RETURN f",
Map.of("name", fruit.name))
.thenCompose(ResultCursor::singleAsync)
.thenApply(record -> Fruit.from(record.get("f").asNode())));
return threadContext.withContextCapture(cs)
.thenCompose(persistedFruit -> session
.closeAsync().thenApply(signal -> persistedFruit))
.thenApply(persistedFruit -> Response
.created(URI.create("/fruits/" + persistedFruit.id))
.build());
As you can see, we are now using a Cypher statement with named parameters (The $name
of the fruit).
The node is returned, a Fruit
entity created and then mapped to a 201
created response.
A curl request against this path may look like this:
curl -v -X "POST" "http://localhost:8080/fruits" \
-H 'Content-Type: application/json; charset=utf-8' \
-d $'{
"name": "Banana"
The response contains an URI that shall return single nodes.
This time, we ask for a read-only transaction.
We also add some exception handling, in case the resource is called with an invalid id:
FruitResource#getSingle
@Path("/{id}")
public CompletionStage<Response> getSingle(String id) {
AsyncSession session = driver.session(AsyncSession.class);
return threadContext.withContextCapture(session
.executeReadAsync(tx -> tx
.runAsync("MATCH (f:Fruit) WHERE f.id = $id RETURN f", Map.of("id", id))
.thenCompose(ResultCursor::singleAsync))
.handle((record, exception) -> {
if (exception != null) {
Throwable source = exception;
if (exception instanceof CompletionException) {
source = exception.getCause();
Status status = Status.INTERNAL_SERVER_ERROR;
if (source instanceof NoSuchRecordException) {
status = Status.NOT_FOUND;
return Response.status(status).build();
} else {
return Response.ok(Fruit.from(record.get("f").asNode())).build();
.thenCompose(response -> session.closeAsync().thenApply(signal -> response));
A request may look like this:
curl localhost:8080/fruits/42
In case Neo4j has been setup as a cluster, the transaction mode is used to decide whether a request is routed
to a leader or a follower instance. Write transactions must be handled by a leader, whereas read-only transactions
can be handled by followers.
@Path("{id}")
public CompletionStage<Response> delete(String id) {
AsyncSession session = driver.session(AsyncSession.class);
return threadContext.withContextCapture(session
.executeWriteAsync(tx -> tx
.runAsync("MATCH (f:Fruit) WHERE f.id = $id DELETE f", Map.of("id", id))
.thenCompose(ResultCursor::consumeAsync) (1)
.thenCompose(response -> session.closeAsync())
.thenApply(signal -> Response.noContent().build());
And that’s already the most simple CRUD application with one type of nodes.
Feel free to add relationships to the model.
One idea would be to model recipes that contain fruits.
The Cypher manual linked in the introduction will help you with modelling your queries.
Packaging your application is as simple as ./mvnw clean package
.
It can be run with java -jar target/quarkus-app/quarkus-run.jar
.
With GraalVM installed, you can also create a native executable binary: ./mvnw clean package -Dnative
.
Depending on your system, that will take some time.
If you are using the quarkus-smallrye-health
extension, quarkus-neo4j
will automatically add a readiness health check
to validate the connection to Neo4j.
So when you access the /q/health/ready
endpoint of your application you will have information about the connection validation status.
This behavior can be disabled by setting the quarkus.neo4j.health.enabled
property to false
in your application.properties
.
If you are using a metrics extension and specify the config property quarkus.neo4j.pool.metrics.enabled=true
, the Neo4j extension will
expose several metrics related to the Neo4j connection pool.
There are tons of options to model your domain within a Graph.
The Neo4j docs, the sandboxes and more are a good starting point.
If you have access to Neo4j 4.0, you can go fully reactive.
To make life a bit easier, we will use Mutiny for this.
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import org.jboss.resteasy.reactive.ResponseStatus;
import org.neo4j.driver.Driver;
import org.neo4j.driver.reactive.ReactiveResult;
import org.neo4j.driver.reactive.ReactiveSession;
import io.smallrye.mutiny.Multi;
import io.smallrye.mutiny.Uni;
@Path("reactivefruits")
@Consumes(MediaType.APPLICATION_JSON)
public class ReactiveFruitResource {
@Inject
Driver driver;
static Uni<Void> sessionFinalizer(ReactiveSession session) { (1)
return Uni.createFrom().publisher(session.close());
@Produces(MediaType.SERVER_SENT_EVENTS)
public Multi<String> get() {
// Create a stream from a resource we can close in a finalizer...
return Multi.createFrom().resource(() -> driver.session(ReactiveSession.class), (2)
session -> session.executeRead(tx -> {
var result = tx.run("MATCH (f:Fruit) RETURN f.name as name ORDER BY f.name");
return Multi.createFrom().publisher(result).flatMap(ReactiveResult::records);
.withFinalizer(ReactiveFruitResource::sessionFinalizer) (3)
.map(record -> record.get("name").asString());
The Multi.createFrom().resource()
is used to defer the creation of session until the publisher is subscribed to
When the publisher is done, the finalizer will be called
driver.rxSession()
returns a reactive session.
It exposes its API based on Reactive Streams, most prominently, as org.reactivestreams.Publisher
.
Those can be used directly, but we found it easier and more expressive to wrap them in reactive types such as the one provided by Mutiny.
Typically, in the previous code, the session is closed when the stream completes, fails or the subscriber cancels.
If you want to return a Mutiny! Uni
object, you need to be very careful before you convert a Multi
into a Uni
:
The conversion works in such a way, that the first item is emitted and then a cancellation signal is sent to the publisher,
that will propagate upto the drivers' session, indicating a cancellation of the transaction, thus doing a rollback.
In most cases you are better off returning a Multi
or just a generic Publisher
. If you need a Uni
, you can still realize
this with an emitter:
ReactiveFruitResource.java#create
@POST
@Produces(MediaType.TEXT_PLAIN)
@ResponseStatus(201)
public Uni<String> create(Fruit fruit) {
return Uni.createFrom().emitter(e -> Multi.createFrom().resource(() -> driver.session(ReactiveSession.class), (1)
session -> session.executeWrite(tx -> {
var result = tx.run(
"CREATE (f:Fruit {id: randomUUID(), name: $name}) RETURN f",
Map.of("name", fruit.name));
return Multi.createFrom().publisher(result).flatMap(ReactiveResult::records);
.withFinalizer(ReactiveFruitResource::sessionFinalizer)
.map(record -> Fruit.from(record.get("f").asNode()))
.toUni()
.subscribe().with( (2)
persistedFruit -> e.complete("/fruits/" + persistedFruit.id)));
Notice how we subscribe to a Multi
setup in a similar fashion as in the prior example. The subscription will emit the one
and only item via the emitter, without a cancellation event.
Each of the neo4j and bolt URI schemes permit variants that contain extra encryption and trust information.
The +s variants enable encryption with a full certificate check, and the +ssc variants enable encryption,
but with no certificate check. This latter variant is designed specifically for use with self-signed certificates.
The variants are basically shortcuts over explicit configuration. If you use one of them, Quarkus won’t pass
quarkus.neo4j.encrypted
and related to the driver creation as the driver prohibits this.
The only check applied when Quarkus detects a secure url (either of +s
or +ssc
) is to ensure availability of
SSL in native image and will throw ConfigurationException
if it isn’t available.
If DevServices has been explicitly enabled or disabled. DevServices is generally enabled by default, unless there is an existing configuration present. When DevServices is enabled Quarkus will attempt to automatically configure and start a database when running in Dev or Test mode.
Environment variable: QUARKUS_NEO4J_DEVSERVICES_ENABLED
boolean
This value can be used to specify the port to which the bolt-port of the container is exposed. It must be a free port, otherwise startup will fail. A random, free port will be used by default. Either way, a messsage will be logged on which port the Neo4j container is reachable over bolt.
Environment variable: QUARKUS_NEO4J_DEVSERVICES_BOLT_PORT
This value can be used to specify the port to which the http-port of the container is exposed. It must be a free port, otherwise startup will fail. A random, free port will be used by default. Either way, a messsage will be logged on which port the Neo4j Browser is available.
Environment variable: QUARKUS_NEO4J_DEVSERVICES_HTTP_PORT
the uri this driver should connect to. The driver supports bolt, bolt+routing or neo4j as schemes
Environment variable: QUARKUS_NEO4J_URI
string
bolt://localhost:7687
quarkus.neo4j.encrypted
An optional field that when is not empty has precedence over username
and password
. It behaves the same way as NEO4J_AUTH
in the official docker image, containing both the username and password separated via a single forward slash (/
).
Environment variable: QUARKUS_NEO4J_AUTHENTICATION_VALUE
string
the trust settings for encrypted traffic
Default
quarkus.neo4j.trust-settings.strategy
Pooled connections that have been idle in the pool for longer than this timeout will be tested before they are used again. The value 0
means connections will always be tested for validity and negative values mean connections will never be tested.
Environment variable: QUARKUS_NEO4J_POOL_IDLE_TIME_BEFORE_CONNECTION_TEST
Duration
-0.001S
quarkus.neo4j.pool.max-connection-lifetime
About the Duration format
To write duration values, use the standard java.time.Duration
format.
See the Duration#parse() Java API documentation for more information.
You can also use a simplified format, starting with a number: