FalkorDB Java client
<dependencies>
<dependency>
<groupId>com.falkordb</groupId>
<artifactId>jfalkordb</artifactId>
<version>0.9.0</version>
</dependency>
</dependencies> <repositories>
<repository>
<id>snapshots-repo</id>
<url>https://oss.sonatype.org/content/repositories/snapshots</url>
</repository>
</repositories>and
<dependencies>
<dependency>
<groupId>com.falkordb</groupId>
<artifactId>jfalkordb</artifactId>
<version>0.10.0-SNAPSHOT</version>
</dependency>
</dependencies>package com.falkordb;
import com.falkordb.graph_entities.Edge;
import com.falkordb.graph_entities.Node;
import com.falkordb.graph_entities.Path;
import com.falkordb.Graph;
import com.falkordb.GraphContext;
import com.falkordb.GraphContextGenerator;
import com.falkordb.GraphTransaction;
import com.falkordb.Record;
import com.falkordb.ResultSet;
import com.falkordb.FalkorDB;
import com.falkordb.Driver;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class GraphExample {
public static void main(String[] args) {
// general context api. Not bound to graph key or connection
Driver driver = FalkorDB.driver();
Graph graph = driver.graph("social");
Map<String, Object> params = new HashMap<>();
params.put("age", 30);
params.put("name", "amit");
// send queries to a specific graph called "social"
graph.query("CREATE (:person{name:'roi',age:32})");
graph.query("CREATE (:person{name:$name,age:$age})", params);
graph.query("MATCH (a:person), (b:person) WHERE (a.name = 'roi' AND b.name='amit') CREATE (a)-[:knows]->(b)");
ResultSet resultSet = graph.query("MATCH (a:person)-[r:knows]->(b:person) RETURN a, r, b");
// iterate over result set
for(Record record: resultSet) {
// get values
Node n = record.getValue("a");
Edge e = record.getValue("r");
//print record
System.out.println("Node: " + n + ", Edge: " + e);
}
resultSet = graph.query("MATCH p = (:person)-[:knows]->(:person) RETURN p");
for(Record record: resultSet) {
Path p = record.getValue("p");
// More path API at Javadoc.
System.out.println(p.nodeCount());
}
// delete graph
graph.deleteGraph();
GraphContextGenerator contextGraph = driver.graph("contextSocial");
// get connection context - closable object
try(GraphContext context = contextGraph.getContext()) {
context.query("CREATE (:person{name:'roi',age:32})");
context.query("MATCH (a:person), (b:person) WHERE (a.name = 'roi' AND b.name='amit') CREATE (a)-[:knows]->(b)");
// WATCH/MULTI/EXEC
context.watch("contextSocial");
GraphTransaction t = context.multi();
t.query("MATCH (a:person)-[r:knows]->(b:person{name:$name,age:$age}) RETURN a, r, b", params);
// support for Redis/Jedis native commands in transaction
t.set("x", "1");
t.get("x");
// get multi/exec results
List<Object> execResults = t.exec();
System.out.println(execResults.toString());
context.deleteGraph();
}
}
}Always pass values as parameters rather than concatenating them into the Cypher string. Parameter values are safely encoded as Cypher literals, so caller-supplied input cannot break out of the literal and inject Cypher:
// safe — the value is encoded, never interpolated into the query text
graph.query("MATCH (p:person {name: $name}) RETURN p", Collections.singletonMap("name", untrustedInput));Values must be an encodable type (null, String, Character, Boolean, a boxed integer or
floating-point number, a BigInteger within signed 64-bit range, or an array/List/Map of such
values, where every Map key is a String); unsupported types (including BigDecimal), out-of-range
integers, non-finite floating-point values, cyclic containers, non-String map keys, and invalid
parameter names are rejected with IllegalArgumentException.
This safety covers parameter values only — it does not extend to the query text, dynamic
labels/identifiers, or procedure names, so never build those from untrusted input.
You can customize the connection pool to optimize performance and resource usage. JFalkorDB uses Jedis internally, which provides comprehensive pool configuration options.
Pool Configuration Guidelines:
maxTotal: Maximum number of connections (size according to your application's concurrency needs)maxIdle: Maximum idle connections kept in the pool (recommended:maxTotal / 4to balance resource usage and responsiveness; increase for applications with high traffic variability)minIdle: Minimum idle connections to keep ready (recommended:maxIdle / 4for steady performance)maxWait: Maximum time to wait for a connection when pool is exhausted
import com.falkordb.Driver;
import com.falkordb.impl.api.DriverImpl;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
import java.time.Duration;
// Create a custom pool configuration
JedisPoolConfig poolConfig = new JedisPoolConfig();
// Maximum number of connections in the pool
poolConfig.setMaxTotal(128);
// Maximum number of idle connections in the pool
// Recommended: Set to maxTotal / 4 for optimal resource usage
poolConfig.setMaxIdle(32); // 128 / 4 = 32
// Minimum number of idle connections in the pool
poolConfig.setMinIdle(8);
// Maximum time to wait for a connection (ms)
poolConfig.setMaxWait(Duration.ofSeconds(30));
// Test connections before borrowing from pool
poolConfig.setTestOnBorrow(true);
// Test connections when returning to pool
poolConfig.setTestOnReturn(true);
// Test idle connections in the pool
poolConfig.setTestWhileIdle(true);
// Create a JedisPool with custom configuration
JedisPool jedisPool = new JedisPool(poolConfig, "localhost", 6379);
// Create the driver with the custom pool
Driver driver = new DriverImpl(jedisPool);
// Use the driver
// ... your code here ...
// Don't forget to close the driver when done
driver.close();import com.falkordb.Driver;
import com.falkordb.impl.api.DriverImpl;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
import redis.clients.jedis.JedisClientConfig;
import redis.clients.jedis.DefaultJedisClientConfig;
import redis.clients.jedis.HostAndPort;
JedisPoolConfig poolConfig = new JedisPoolConfig();
poolConfig.setMaxTotal(64);
poolConfig.setMaxIdle(16); // 64 / 4 = 16
poolConfig.setMinIdle(4);
// Configure authentication
JedisClientConfig clientConfig = DefaultJedisClientConfig.builder()
.user("default") // username
.password("your-password") // password
.build();
// Create pool with authentication and custom pool config
HostAndPort hostAndPort = new HostAndPort("localhost", 6379);
JedisPool jedisPool = new JedisPool(poolConfig, hostAndPort, clientConfig);
Driver driver = new DriverImpl(jedisPool);import com.falkordb.Driver;
import com.falkordb.impl.api.DriverImpl;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
import redis.clients.jedis.JedisClientConfig;
import redis.clients.jedis.DefaultJedisClientConfig;
import redis.clients.jedis.HostAndPort;
import java.time.Duration;
JedisPoolConfig poolConfig = new JedisPoolConfig();
poolConfig.setMaxTotal(128);
poolConfig.setMaxIdle(32); // 128 / 4 = 32
poolConfig.setMinIdle(8);
poolConfig.setMaxWait(Duration.ofSeconds(30));
// Configure connection and socket timeouts
JedisClientConfig clientConfig = DefaultJedisClientConfig.builder()
.connectionTimeoutMillis(2000) // Connection timeout: time to establish connection
.socketTimeoutMillis(5000) // Socket timeout: time to wait for response
.user("default") // Username for authentication
.password("your-password") // Password for authentication
.build();
// Create pool with advanced configuration
HostAndPort hostAndPort = new HostAndPort("localhost", 6379);
JedisPool jedisPool = new JedisPool(poolConfig, hostAndPort, clientConfig);
Driver driver = new DriverImpl(jedisPool);import com.falkordb.Driver;
import com.falkordb.impl.api.DriverImpl;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
import redis.clients.jedis.JedisClientConfig;
import redis.clients.jedis.DefaultJedisClientConfig;
import redis.clients.jedis.HostAndPort;
import javax.net.ssl.SSLSocketFactory;
JedisPoolConfig poolConfig = new JedisPoolConfig();
poolConfig.setMaxTotal(64);
poolConfig.setMaxIdle(16); // 64 / 4 = 16
poolConfig.setMinIdle(4);
// Configure SSL connection
JedisClientConfig clientConfig = DefaultJedisClientConfig.builder()
.ssl(true)
.sslSocketFactory((SSLSocketFactory) SSLSocketFactory.getDefault())
.user("default")
.password("your-password")
.build();
HostAndPort hostAndPort = new HostAndPort("your-server.com", 6380);
JedisPool jedisPool = new JedisPool(poolConfig, hostAndPort, clientConfig);
Driver driver = new DriverImpl(jedisPool);For more information about Jedis pool configuration options, see the Jedis documentation.
JFalkorDB is a blocking client, which makes it an excellent fit for Java 21+ virtual threads: run each query on its own virtual thread and let many of them block concurrently at near-async throughput — with no API change. The library itself stays Java 8; you supply the executor, so this is purely a consumer-side choice.
driver.graph(id) returns a pool-per-call GraphContextGenerator that is safe to call concurrently, so
you can share one instance across many virtual threads:
import com.falkordb.Driver;
import com.falkordb.FalkorDB;
import com.falkordb.GraphContextGenerator;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
int concurrency = 64;
try (Driver driver = FalkorDB.builder().host("localhost").port(6379)
.poolMaxTotal(concurrency) // real concurrency is bounded by the pool, not the thread count
.poolMaxIdle(concurrency)
.build()) {
GraphContextGenerator graph = driver.graph("social");
ExecutorService vthreads = Executors.newVirtualThreadPerTaskExecutor(); // JDK 21+
try {
for (int i = 0; i < 1000; i++) {
int id = i;
vthreads.submit(() -> graph.query("MATCH (n) WHERE n.id = $id RETURN n",
java.util.Collections.singletonMap("id", id)));
}
} finally {
vthreads.shutdown();
vthreads.awaitTermination(1, java.util.concurrent.TimeUnit.MINUTES); // drain in-flight queries before the pool closes
}
}If you prefer CompletableFutures, wrap the graph with AsyncFalkorDB.wrap(graph, executor). Every
method mirrors the synchronous Graph operation but runs on your executor and returns a future:
import com.falkordb.AsyncFalkorDB;
import com.falkordb.AsyncGraph;
import com.falkordb.Driver;
import com.falkordb.FalkorDB;
import com.falkordb.ResultSet;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
Driver driver = FalkorDB.builder().host("localhost").port(6379).build();
ExecutorService vthreads = Executors.newVirtualThreadPerTaskExecutor(); // JDK 21+
AsyncGraph async = AsyncFalkorDB.wrap(driver.graph("social"), vthreads);
CompletableFuture<ResultSet> future = async.query("MATCH (n) RETURN count(n)");
ResultSet result = future.join(); // or compose with thenApply/thenCompose
// on shutdown: drain outstanding futures, then vthreads.shutdown() and driver.close()The facade owns nothing — it is not Closeable; the caller shuts the executor down and closes the
underlying Driver (which releases the pooled connections) when done. Failures surface as the future
completing exceptionally (join() throws CompletionException, get() throws ExecutionException).
Cancellation is best-effort: cancelling before a task starts skips the query, but an in-flight blocking
read cannot be interrupted — bound it with socketTimeout instead (see below).
Real database concurrency is bounded by the connection pool, not by how many virtual threads you
create: at most poolMaxTotal queries run against the server at once, and the default is small
(commons-pool2's default maxTotal is 8). Size poolMaxTotal to your target concurrency N:
- There is no built-in admission bound — a virtual-thread executor accepts unbounded tasks while DB
concurrency stays pool-limited. Add your own
Semaphoreif you need to cap in-flight work. - Set a finite
poolMaxWaitso a saturated pool fails fast (throws) instead of blocking forever; set a finitesocketTimeoutso a stuck read cannot hang a worker.
On JDK 21–23, creating a brand-new pooled connection briefly pins the carrier thread (the
underlying commons-pool2 create() is synchronized), so a burst of first-time connections under a
virtual-thread fan-out can momentarily starve carriers. Avoid it by warming the pool up front —
borrow and hold N connections once (on platform threads) so the pool creates them before the workload,
sizing both poolMaxTotal >= N and poolMaxIdle >= N so they are retained as idle:
// Warm N connections before the virtual-thread workload (poolMaxTotal == poolMaxIdle == N).
Driver driver = FalkorDB.builder().host("localhost").port(6379)
.poolMaxTotal(64).poolMaxIdle(64).build();
GraphContextGenerator graph = driver.graph("social");
int n = 64;
var pool = Executors.newFixedThreadPool(n); // platform threads: cold creation must not pin virtual threads
var release = new java.util.concurrent.CountDownLatch(1);
var ready = new java.util.concurrent.CountDownLatch(n);
for (int i = 0; i < n; i++) {
pool.submit(() -> {
try (var ctx = graph.getContext()) { // borrows a connection
ctx.query("RETURN 1");
ready.countDown();
release.await(); // hold so all N are open at once => N distinct connections
}
return null;
});
}
ready.await(30, java.util.concurrent.TimeUnit.SECONDS); // (bound the waits and check results in production)
release.countDown();
pool.shutdown();
if (!pool.awaitTermination(30, java.util.concurrent.TimeUnit.SECONDS)) { // wait for connections to return
pool.shutdownNow();
}The client's own code holds no lock across a blocking call — a scheduled CI pinning check verifies no
carrier pin originates in com.falkordb.impl under a virtual-thread load — and JDK 24+ removes carrier
pinning for synchronized entirely (JEP 491), so this warm-up matters
only on JDK 21–23.
Continuous client load-sweep benchmarks run on every push to master and on pull requests
targeting master. They measure the client-side cost — total round-trip minus the server's
reported internal execution time — as concurrency (and therefore throughput) increases, so
connection/thread-management effects are visible. Interactive charts are published to GitHub Pages:
📈 JFalkorDB benchmarks — a latency-vs-throughput saturation curve plus per-PR latency & throughput trends.
Run them locally with just bench (see CONTRIBUTING.md).
JFalkorDB is licensed under the BSD-3-Clause license .