Using TidesDB with Java via JExtract

by Alex Gaetano Padula
published on December 31th, 2025
It’s come to my attention recently that TidesDB had issues being binded to. This was brought up by an awesome user on the TidesDB Discord server.
The main issue was that the main tidesdb.h header contained too many unrequired structs, and types for an FFI/FFM. The generator could not handle the structure definitions and ended up with up errors such like below:
source.h:200:1: warning: Skipping _dispatch_source_type_vnode (type Declared(dispatch_source_type_s) is not supported)source.h:211:1: warning: Skipping _dispatch_source_type_write (type Declared(dispatch_source_type_s) is not supported)data.h:55:40: warning: Skipping _dispatch_data_empty (type Declared(dispatch_data_s) is not supported)math.h:65:15: warning: Skipping HUGE_VALL (type LongDouble is not supported)fatal: Unexpected exception java.lang.IllegalArgumentException: Type not supported: ATOMIC = (typedef Optional[queue_node_t] = Declared(queue_node_t))* occurredTo fix the issue I created a new db.h header file that only contains opaque pointers for all structs (avoiding the problematic internal atomic fields which were causing issues). I’ve exposed only the public API functions that are absolutely necessary. No _Atomic types which seemed to be unsupported by jextract.
With that, the layout of the db.h is simpler for a generator to take and parse.
You can find the db.h header file here. This update has been merged and is part of the v7.0.6 PATCH.
If you are going to follow along you will need Java 21 or later as it is required for Foreign Function & Memory API, the TidesDB shared library (v7.0.6), and the jextract tool. I am also using Ubuntu for this example.
Now let’s get into how you can utilize TidesDB’s C API with Java.
Once I’ve built and installed the TidesDB shared library, I can do the following in a specific directory in my case i used tidesdb-java.
jextract -t com.tidesdb.tidesdb -l tidesdb --output src /usr/local/include/tidesdb/db.hjextract [options] <header-file>
- t <package> · Target package name
- l <library> · Native/Shared library to link against
- output <dir> · Output directory
What this will do is generate a set of java files in the src directory that can be used to bind to the TidesDB shared library.
/tidesdb-java/src/com/tidesdb/tidesdb$
db_h.java tidesdb_column_family_config_t.java tidesdb_stats_t.java__fsid_t.java tidesdb_column_family_t.java tidesdb_t.javaitimerspec.java tidesdb_comparator_fn.java tidesdb_txn_t.java__locale_struct.java tidesdb_config_t.java timespec.javamax_align_t.java tidesdb_iter_t.java tm.javadb_h.java is the main entry point with all the native functions.
Now we are going to go through how you can use TidesDB with Java.
Opening a Database
You need to import the necessary classes and packages. In this case we need to import the com.tidesdb.tidesdb package and the java.lang.foreign package.
Once we do that we can create a new arena and open a database. TidesDB requires a configuration struct to be passed in to the open function. We can use the default config struct and modify it as needed.
import com.tidesdb.tidesdb.*;import java.lang.foreign.*;
public class TidesDBExample { public static void main(String[] args) { try (Arena arena = Arena.ofConfined()) {
MemorySegment config = db_h.tidesdb_default_config(arena);
// Set database path MemorySegment dbPath = arena.allocateUtf8String("/path/to/database"); tidesdb_config_t.db_path(config, dbPath);
// Configure threads and cache tidesdb_config_t.num_flush_threads(config, 4); tidesdb_config_t.num_compaction_threads(config, 2); tidesdb_config_t.block_cache_size(config, 1024 * 1024 * 100); // 100MB tidesdb_config_t.max_open_sstables(config, 100);
MemorySegment dbPtr = arena.allocate(ValueLayout.ADDRESS); int result = db_h.tidesdb_open(config, dbPtr);
if (result != db_h.TDB_SUCCESS()) { System.err.println("Failed to open database"); return; }
MemorySegment db = dbPtr.get(ValueLayout.ADDRESS, 0);
// Use the database...
db_h.tidesdb_close(db); } }}Creating a Column Family
Once we have a database open we can create a column family. TidesDB requires a column family configuration struct to be passed in to the create function. We can use the default column family config struct and modify it as needed.
// Get default column family configuration, is optimized for performanceMemorySegment cfConfig = db_h.tidesdb_default_column_family_config(arena);
// Configure column family settingstidesdb_column_family_config_t.write_buffer_size(cfConfig, 1024 * 1024 * 64); // 64MBtidesdb_column_family_config_t.level_size_ratio(cfConfig, 10);tidesdb_column_family_config_t.min_levels(cfConfig, 3);tidesdb_column_family_config_t.enable_bloom_filter(cfConfig, 1); // Enabletidesdb_column_family_config_t.bloom_fpr(cfConfig, 0.01); // 1% false positive rate
// Set compression algorithm// Available compression algorithms:// - NO_COMPRESSION (0)// - SNAPPY_COMPRESSION (1)// - LZ4_COMPRESSION (2)// - ZSTD_COMPRESSION (3)tidesdb_column_family_config_t.compression_algo(cfConfig, db_h.SNAPPY_COMPRESSION());
tidesdb_column_family_config_t.enable_block_indexes(cfConfig, 1);
// Create a new column familyMemorySegment cfName = arena.allocateUtf8String("my_column_family");int result = db_h.tidesdb_create_column_family(db, cfName, cfConfig);
if (result != db_h.TDB_SUCCESS()) { System.err.println("Failed to create column family");}Getting a Column Family
Once we have a database open with a created column family, we can get a column family. TidesDB requires a column family name to be passed in to the get function.
MemorySegment cfName = arena.allocateUtf8String("my_column_family");MemorySegment cf = db_h.tidesdb_get_column_family(db, cfName);
if (cf.address() == 0) { System.err.println("Column family not found");}Working with Transactions
Once you have column families, you can start to utilize transactions. TidesDB is fully transactional and supports ACID properties and many isolation levels.
Begin a Transaction
You can begin a transaction with a default isolation level of TDB_ISOLATION_READ_COMMITTED. From there add operations from different column families to the transaction.
// Begin transaction with default isolation level (TDB_ISOLATION_READ_COMMITTED)MemorySegment txnPtr = arena.allocate(ValueLayout.ADDRESS);int result = db_h.tidesdb_txn_begin(db, txnPtr);
if (result != db_h.TDB_SUCCESS()) { System.err.println("Failed to begin transaction"); return;}
MemorySegment txn = txnPtr.get(ValueLayout.ADDRESS, 0);Put Operation (Insert/Update)
You can add put operations to the transaction. TidesDB supports TTL (Time To Live) for keys. You can set a TTL of 0 to disable expiration. TTL is in seconds.
// Prepare key and valueString keyStr = "user:1001";String valueStr = "John Doe";
MemorySegment key = arena.allocateUtf8String(keyStr);MemorySegment value = arena.allocateUtf8String(valueStr);
// Put with TTL (0 = no expiration)long ttl = 0;int result = db_h.tidesdb_txn_put( txn, cf, key, keyStr.length(), value, valueStr.length(), ttl);
if (result != db_h.TDB_SUCCESS()) { System.err.println("Failed to put key-value");}Get Operation (Retrieve)
You can add get operations to the transaction. TidesDB supports retrieving values from a column family.
String keyStr = "user:1001";MemorySegment key = arena.allocateUtf8String(keyStr);
// Allocate pointers for outputMemorySegment valuePtr = arena.allocate(ValueLayout.ADDRESS);MemorySegment valueSizePtr = arena.allocate(ValueLayout.JAVA_LONG);
int result = db_h.tidesdb_txn_get( txn, cf, key, keyStr.length(), valuePtr, valueSizePtr);
if (result == db_h.TDB_SUCCESS()) { MemorySegment valueData = valuePtr.get(ValueLayout.ADDRESS, 0); long valueSize = valueSizePtr.get(ValueLayout.JAVA_LONG, 0);
// Read the value byte[] valueBytes = new byte[(int) valueSize]; MemorySegment.copy(valueData, ValueLayout.JAVA_BYTE, 0, valueBytes, 0, (int) valueSize); String value = new String(valueBytes);
System.out.println("Retrieved value: " + value);
} else { System.err.println("Key not found or error occurred");}Delete Operation
You can add delete operations to the transaction.
String keyStr = "user:1001";MemorySegment key = arena.allocateUtf8String(keyStr);
int result = db_h.tidesdb_txn_delete( txn, cf, key, keyStr.length());
if (result != db_h.TDB_SUCCESS()) { System.err.println("Failed to delete key");}Commit Transaction
To apply all the operations in the transaction you need to commit it.
int result = db_h.tidesdb_txn_commit(txn);
if (result != db_h.TDB_SUCCESS()) { System.err.println("Failed to commit transaction");}Rollback Transaction
To undo all the operations in the transaction you can choose to rollback it.
int result = db_h.tidesdb_txn_rollback(txn);
if (result != db_h.TDB_SUCCESS()) { System.err.println("Failed to rollback transaction");}Transaction Savepoints
Savepoints allow you to create checkpoints within a transaction that you can rollback to without rolling back the entire transaction.
Create a Savepoint
MemorySegment savepointName = arena.allocateUtf8String("checkpoint1");int result = db_h.tidesdb_txn_savepoint(txn, savepointName);
if (result != db_h.TDB_SUCCESS()) { System.err.println("Failed to create savepoint");}Rollback to a Savepoint
MemorySegment savepointName = arena.allocateUtf8String("checkpoint1");int result = db_h.tidesdb_txn_rollback_to_savepoint(txn, savepointName);
if (result != db_h.TDB_SUCCESS()) { System.err.println("Failed to rollback to savepoint");}Release a Savepoint
MemorySegment savepointName = arena.allocateUtf8String("checkpoint1");int result = db_h.tidesdb_txn_release_savepoint(txn, savepointName);
if (result != db_h.TDB_SUCCESS()) { System.err.println("Failed to release savepoint");}Transaction with Custom Isolation Level
// - TDB_ISOLATION_READ_UNCOMMITTED (0)// - TDB_ISOLATION_READ_COMMITTED (1)// - TDB_ISOLATION_REPEATABLE_READ (2)// - TDB_ISOLATION_SNAPSHOT (3)// - TDB_ISOLATION_SERIALIZABLE (4)int isolationLevel = db_h.TDB_ISOLATION_SERIALIZABLE();MemorySegment txnPtr = arena.allocate(ValueLayout.ADDRESS);
int result = db_h.tidesdb_txn_begin_with_isolation(db, isolationLevel, txnPtr);
if (result != db_h.TDB_SUCCESS()) { System.err.println("Failed to begin transaction with isolation level"); return;}
MemorySegment txn = txnPtr.get(ValueLayout.ADDRESS, 0);Using Iterators
Iterators allow you to scan through keys in a column family. They’re essential for range queries and scanning operations.
Creating an Iterator
MemorySegment iterPtr = arena.allocate(ValueLayout.ADDRESS);int result = db_h.tidesdb_iter_new(txn, cf, iterPtr);
if (result != db_h.TDB_SUCCESS()) { System.err.println("Failed to create iterator"); return;}
MemorySegment iter = iterPtr.get(ValueLayout.ADDRESS, 0);Seeking to First Entry
int result = db_h.tidesdb_iter_seek_to_first(iter);
if (result != db_h.TDB_SUCCESS()) { System.err.println("Failed to seek to first");}Seeking to Last Entry
int result = db_h.tidesdb_iter_seek_to_last(iter);
if (result != db_h.TDB_SUCCESS()) { System.err.println("Failed to seek to last");}Seeking to Specific Key
String keyStr = "user:1000";MemorySegment key = arena.allocateUtf8String(keyStr);
int result = db_h.tidesdb_iter_seek(iter, key, keyStr.length());
if (result != db_h.TDB_SUCCESS()) { System.err.println("Failed to seek to key");}Iterating Through Entries
// Seek to first entrydb_h.tidesdb_iter_seek_to_first(iter);
// Iterate through all entrieswhile (db_h.tidesdb_iter_valid(iter) != 0) { // Get key MemorySegment keyPtr = arena.allocate(ValueLayout.ADDRESS); MemorySegment keySizePtr = arena.allocate(ValueLayout.JAVA_LONG);
if (db_h.tidesdb_iter_key(iter, keyPtr, keySizePtr) == db_h.TDB_SUCCESS()) { MemorySegment keyData = keyPtr.get(ValueLayout.ADDRESS, 0); long keySize = keySizePtr.get(ValueLayout.JAVA_LONG, 0);
byte[] keyBytes = new byte[(int) keySize]; MemorySegment.copy(keyData, ValueLayout.JAVA_BYTE, 0, keyBytes, 0, (int) keySize); String keyStr = new String(keyBytes);
// Get value MemorySegment valuePtr = arena.allocate(ValueLayout.ADDRESS); MemorySegment valueSizePtr = arena.allocate(ValueLayout.JAVA_LONG);
if (db_h.tidesdb_iter_value(iter, valuePtr, valueSizePtr) == db_h.TDB_SUCCESS()) { MemorySegment valueData = valuePtr.get(ValueLayout.ADDRESS, 0); long valueSize = valueSizePtr.get(ValueLayout.JAVA_LONG, 0);
byte[] valueBytes = new byte[(int) valueSize]; MemorySegment.copy(valueData, ValueLayout.JAVA_BYTE, 0, valueBytes, 0, (int) valueSize); String valueStr = new String(valueBytes);
System.out.println("Key: " + keyStr + ", Value: " + valueStr); } }
// Move to next entry db_h.tidesdb_iter_next(iter);}Iterating in Reverse
// Seek to last entrydb_h.tidesdb_iter_seek_to_last(iter);
// Iterate backwardswhile (db_h.tidesdb_iter_valid(iter) != 0) { // Process entry...
// Move to previous entry db_h.tidesdb_iter_prev(iter);}Freeing an Iterator
db_h.tidesdb_iter_free(iter);Full Example
import com.tidesdb.tidesdb.*;import java.lang.foreign.*;
public class TidesDBCompleteExample { public static void main(String[] args) { try (Arena arena = Arena.ofConfined()) { // Open a new database MemorySegment config = db_h.tidesdb_default_config(arena); MemorySegment dbPath = arena.allocateUtf8String("./mydb"); tidesdb_config_t.db_path(config, dbPath);
MemorySegment dbPtr = arena.allocate(ValueLayout.ADDRESS); if (db_h.tidesdb_open(config, dbPtr) != db_h.TDB_SUCCESS()) { System.err.println("Failed to open database"); return; } MemorySegment db = dbPtr.get(ValueLayout.ADDRESS, 0);
MemorySegment cfConfig = db_h.tidesdb_default_column_family_config(arena); MemorySegment cfName = arena.allocateUtf8String("users");
if (db_h.tidesdb_create_column_family(db, cfName, cfConfig) != db_h.TDB_SUCCESS()) { System.out.println("Column family might already exist"); }
MemorySegment cf = db_h.tidesdb_get_column_family(db, cfName);
// Begin transaction MemorySegment txnPtr = arena.allocate(ValueLayout.ADDRESS); if (db_h.tidesdb_txn_begin(db, txnPtr) != db_h.TDB_SUCCESS()) { System.err.println("Failed to begin transaction"); db_h.tidesdb_close(db); return; } MemorySegment txn = txnPtr.get(ValueLayout.ADDRESS, 0);
// Write data String key = "user:1"; String value = "Alice"; MemorySegment keyMem = arena.allocateUtf8String(key); MemorySegment valueMem = arena.allocateUtf8String(value);
if (db_h.tidesdb_txn_put(txn, cf, keyMem, key.length(), valueMem, value.length(), 0) == db_h.TDB_SUCCESS()) { System.out.println("Put successful"); }
// Retrieve data MemorySegment valuePtr = arena.allocate(ValueLayout.ADDRESS); MemorySegment valueSizePtr = arena.allocate(ValueLayout.JAVA_LONG);
if (db_h.tidesdb_txn_get(txn, cf, keyMem, key.length(), valuePtr, valueSizePtr) == db_h.TDB_SUCCESS()) { MemorySegment valueData = valuePtr.get(ValueLayout.ADDRESS, 0); long valueSize = valueSizePtr.get(ValueLayout.JAVA_LONG, 0);
byte[] valueBytes = new byte[(int) valueSize]; MemorySegment.copy(valueData, ValueLayout.JAVA_BYTE, 0, valueBytes, 0, (int) valueSize); System.out.println("Retrieved: " + new String(valueBytes)); }
if (db_h.tidesdb_txn_commit(txn) == db_h.TDB_SUCCESS()) { System.out.println("Transaction committed"); }
db_h.tidesdb_close(db);
} catch (Exception e) { e.printStackTrace(); } }}Configuration Options
Database Configuration
db_path· Path to database directorynum_flush_threads· Number of threads for flushing memtablesnum_compaction_threads· Number of threads for compactionlog_level· Logging levelblock_cache_size· Size of block cache in bytesmax_open_sstables· Maximum number of open SSTable files
Column Family Configuration
write_buffer_size· Size of write buffer (memtable)level_size_ratio· Size ratio between levelsmin_levels· Minimum number of levelsdividing_level_offset· Level offset for dividingklog_value_threshold· Threshold for key-log valuescompression_algo· Compression algorithm to useenable_bloom_filter· Enable/disable bloom filtersbloom_fpr· Bloom filter false positive rateenable_block_indexes· Enable/disable block indexesindex_sample_ratio· Index sampling ratioblock_index_prefix_len· Block index prefix lengthsync_mode· Synchronization modesync_interval_us· Sync interval in microsecondsskip_list_max_level· Maximum skip list levelskip_list_probability· Skip list probabilitydefault_isolation_level· Default transaction isolation levelmin_disk_space· Minimum required disk spacel1_file_count_trigger· L1 compaction triggerl0_queue_stall_threshold· L0 queue stall threshold
Error Handling
All TidesDB functions return an integer status code:
TDB_SUCCESS(0) · Operation succeeded- Non-zero · Error occurred (check TidesDB documentation here)
Always check return values
int result = db_h.tidesdb_open(config, dbPtr);if (result != db_h.TDB_SUCCESS()) { // Handle error}A little note, arena manages native memory allocation. Using try-with-resources ensures all native memory allocated through the arena is automatically freed when the block exits, preventing memory leaks. This is crucial when working with native libraries.
Running Your Application
Compile and run with Java 21+
javac --enable-preview -cp . YourApp.javajava --enable-preview -Djava.library.path=/path/to/tidesdb/lib YourAppEnd
As you can see it’s not too hard to get going with TidesDB and Java, I’d imagine as I’ve seen this done with TidesDB in other languages like Rust (bindgen), this process is fast and easy. Maybe down the line I’ll work on a Rust version of this article.
Thanks for reading!