Query Java objects like a database, right inside your application.
An embedded object database for Java 17+. Store plain annotated objects and query them with a type-safe, Stream-like API or SQL-style OQL, answered from indexes in milliseconds. Data is crash-safe on disk, with transactions at three isolation levels, unique and foreign-key constraints, exact decimals and online backup.
First Studio, operated with the mouse: browse, filter, inspect, export, follow references, edit (form and JSON), constraints, delete with cascade, transactions with commit and rollback, OQL with EXPLAIN, and backup. Then the same features as Java code. English subtitles.
Typical pain points when an application needs to keep and query its own objects:
Mapping layers, schemas and a database server just to persist and filter your own objects.
list.stream().filter(...) scans everything, every time, and lives only in the heap.
Field names in strings break silently when you refactor.
Binary stores give you nothing to browse, fix or export data with.
Duplicate keys and dangling references slip in when every check lives in application code.
"Check, then write" breaks under concurrency: two writers both see the old state.
0.1 + 0.2 is not 0.3 in a double.
Copying files while the application writes gives an inconsistent copy.
Embedded, durable, indexed and queryable, with a desktop tool to look inside.
query(Book.class).where(Book::getYear).ge(1960).list() or the generated Book_ metamodel. Refactoring-safe.
Index the fields you filter, sort and group by: milliseconds on 10 M objects.
SELECT / UPDATE / DELETE with the SQL-92 core: CASE, LIKE, DISTINCT, GROUP BY โฆ HAVING.
Inheritance, embedded objects at any depth, lists and maps of them (whereAny, MEMBER OF), references with fetch.
READ COMMITTED, SNAPSHOT, SERIALIZABLE; optimistic, never blocking; retries built in.
@Unique on a field or a combination of fields; checked atomically at commit.
@ForeignKey with RESTRICT, CASCADE or SET_NULL; backlinks with referrers.
BigDecimal fields, indexed and compared by value; exact SUM and arithmetic.
@PrePersist, @PostLoad and friends, plus entity listeners.
A consistent snapshot while writers keep going; restore = open.
Long texts and binaries such as images, several MiB per value.
Committed data survives crashes; readers see consistent snapshots; deleted space is reclaimed.
A self-describing schema with evolution. Open any database without its classes.
About 150 B per object on disk; opens in milliseconds with a tiny heap.
Several databases, transactions at any level, backlinks, constraints, image preview, OQL console, backup. One jar for every OS.
Every shared suite runs in memory and on disk; โฅ 90% coverage; a 10 M-object stress run.
explain() shows which one.db.backup(dir) for a consistent copy while the application runs.Add the dependency, annotate a class, query it.
<dependency>
<groupId>com.github.paganini2008</groupId>
<artifactId>kookaburra-core</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>@Entity public class Book { @Id long oid; @Version long version; @Index String title; @Index Genre genre; @Index(IndexKind.ORDINAL) int year; double price; // constructors, getters โฆ }
try (ObjectDB db = ObjectDB.open(Path.of("data"))) { db.batch() .insert(new Book("Dune", Genre.SCIFI, 1965, 9.99)) .insert(new Book("Neuromancer", Genre.SCIFI, 1984, 12.50)) .insert(new Book("Hyperion", Genre.SCIFI, 1989, 14.20)) .commit(); List<Book> books = db.query(Book.class) .where(Book::getGenre).eq(Genre.SCIFI) .where(Book::getYear).ge(1960) .orderBy(Book::getYear).desc() .limit(2) .list(); System.out.println(books.stream().map(Book::getTitle).toList()); }
[Hyperion, Neuromancer]
Each example: input โ code โ output, from a real run on six books.
Count and average price per genre.
db.query(Book.class)
.groupBy(where(Book::getGenre))
.agg(Q.count().as("books"),
Q.avg(where(Book::getPrice)).as("avgPrice"))
.toList();
Text queries with parameters.
db.oql("SELECT b.author, b.year FROM Book b " +
"WHERE b.year < :y ORDER BY b.year")
.param("y", 1950).rows();
Compile-time field constants from the annotation processor.
db.query(Book.class)
.filter(Book_.tags.contains("classic"))
.filter(Book_.price.lt(10.0))
.count();
Dynamic access to any stored class.
DynamicCollection books = db.dynamic("Book");
books.from()
.where("author").startsWith("J")
.list().get(0).get("title");
Optimistic locking with @Version.
dune.setPrice(11.99); db.update(dune); // OptimisticLockException if changed meanwhile
Which index answers each condition.
db.query(Book.class) .where(Book::getGenre).eq(SCIFI) .where(Book::getYear).between(1950, 1970) .explain();
Opt-in transactions; conflicts are detected at commit.
try (Transaction tx = db.begin(Isolation.SNAPSHOT)) {
Account a = tx.get(Account.class, from);
tx.update(a.balance(a.getBalance() - 30));
tx.increment(Account.class, to, "balance", 30);
tx.commit();
}
Paths into embedded objects, lists and maps.
db.query(Shipment.class)
.whereAny(Shipment::getItems, Item::getSku)
.contains("pear").list();
db.oql("SELECT s FROM Shipment s " +
"WHERE 'kiwi' MEMBER OF s.items.sku").list();
Large values next to their metadata.
db.insert(new Document("scan", text,
Files.readAllBytes(Path.of("scan.png"))));
// Base64 in JSON / CSV exports
Unique numbers, cascading foreign keys, exact totals.
@Unique String number; @ForeignKey(onDelete = OnDelete.CASCADE) Ref<Customer> customer; @Index BigDecimal total; db.query(Order.class).sum(where(Order::getTotal));
Two on-call doctors both try to go off call.
db.inTransaction(TxOptions.defaults()
.isolation(Isolation.SERIALIZABLE), tx -> {
if (tx.query(Doctor.class).where(Doctor::isOnCall)
.eq(true).count() > 1)
tx.update(tx.get(Doctor.class, me).offCall());
});
Back up while writing; open the copy to restore.
BackupInfo info = db.backup(
Path.of("backups/2026-10-17"));
try (ObjectDB copy = ObjectDB.open(info.path())) {
โฆ
}




A standalone desktop tool: several databases side by side, transactions, constraints and backlinks, nested objects and binaries, an OQL console, backups.

BEGIN ISOLATION LEVEL โฆ; COMMIT in the console.

UNIQUE, FK โ Customer ยท CASCADE.
[ n elements ]; the Key / Value / Type inspector expands every level.

p50 latency from the kookaburra-bench stress harness (warm-up, repeated runs, consistency checks after every phase).
| Metric | Result |
|---|---|
| bulk insert, batches of 100 (ASYNC) | 696 k/s |
| durable inserts, 16 / 128 threads (SYNC) | 3.2 / 21.2 k/s |
| compact 10 M objects (1.4 GB) | 10.4 s |
| online backup of 10.5 M objects, writers running | 25.4 s |
| transfers, 16 threads: SNAPSHOT / RC / SERIALIZABLE | 136 / 142 / 140 k/s |
| insert with @Unique / without | 235 / 258 k/s |
| atomic increments on one hot counter | 265 k/s |
| OQL DELETE of 10.5 M objects, disk after compact | 1.32 M/s, 0 B |
| reopen | 2.0 ms |
| heap after reopen | 6.5 MiB |
| disk footprint | 148 B / obj |
Apple M2 Max ยท 32 GB ยท SSD ยท JDK 17 ยท -Xmx8g. Durable single writes scale with concurrent writers, which share disk flushes. Full report โ
Three minutes from entity to Studio.
Dependency, first entity, first query.
Typed API, OQL, constraints, isolation, backup.
One kookaburra.properties for every setting.
QueryStream, fields, OQL grammar, dynamic API.
Overall and storage design, for contributors.
Three isolation levels with OCC, decisions T1 to T17.
Build, test, conventions and docs tooling.