ZeroZ DB · The zero-impedance database

You already have a data model. Why describe it twice?

Every entity in a conventional Java application exists as at least two things, the class and the table, plus a mapping layer whose whole job is to stop the two versions of one idea from drifting apart. ZeroZ DB simply deletes the second version, so there is only the class.

Artifactcom.zeroz4j:zerozdb
RuntimeJava 21 · pure Java, no native code
LicenceApache 2.0

The cost

Two schemas for one idea.

The database mismatch

Your domain model is written in Java, but it is stored in relational tables, so you need SQL or JPA/Hibernate to translate between the two. That means you maintain two descriptions of the same idea (the class and the table) and keep them in step by hand.

The mapping layer is a cost you never stop paying

It expresses no business rule and produces no feature, and yet you maintain it for the whole life of the system. In any single sprint the cost is invisible, but added up over ten years it is usually one of the bigger line items.

The queries are in a different language

The compiler cannot check them and the IDE cannot refactor through them, so a rename in Java leaves behind a string in a repository that still compiles fine and now quietly returns nothing.

The performance problems are built in

N+1 selects, lazy-loading exceptions outside the session, and caches that exist entirely to undo the cost of a translation you did not want in the first place, are all consequences of the mapping itself, so no amount of tuning makes them go away completely.

The solution

Your objects are the database.

ZeroZ DB stores a Java object graph directly. A root object holds your maps and lists, those hold your entities, and you navigate them with ordinary field access and query them with streams. The graph lives in the heap and runs at heap speed, and it is written to disk as binary. There is no schema to declare, no mapping to configure and no query language to learn, basically because there is nothing between your classes and the storage.

SERVER · ProspectService.java● The real API
ZeroZDb db = ZeroZDb.open(new MyRoot(), Path.of("data/mystore"));
MyRoot root = db.root();

db.write(ctx -> {                       // serialized, atomic, durable on return
    root.prospects().put(id, prospect); // a plain Java mutation
    ctx.store(root.prospects());
});

List<Prospect> hot = db.read(() ->      // concurrent, never sees torn state
    root.prospects().values().stream().filter(Prospect::isHot).toList());

If it looks like the database is missing from that code, that is essentially the point. The write block is the transaction and the stream is the query.

02 · Guarantees

What a real database has to give you.

Object-graph persistence on its own is a storage engine and not yet a database. ZeroZ DB is the part that has to be added before you can put a system of record on top of one, and this section lists what that part does.

Atomic transactions

Every write runs inside a block that either commits entirely or leaves the graph exactly as it was. Writes are serialised against one another, so one commit is never interleaved with another. A rollback restores the in-memory objects from before-images taken at enlistment, which is the reason the API asks you to enlist an object before you change it.

Durability that actually calls fsync

Most plain object-graph engines return from a commit when the operating system has accepted the bytes, not when the disk actually has them, so the data survives a process crash but not a power cut. ZeroZ DB forces the channel after every storage write by default. The weaker but faster behaviour is still there for bulk loads, but you have to ask for it explicitly.

Concurrent readers

Readers run against the live graph without blocking one another and without ever seeing a half-applied write. The read/write lock is deliberately fair rather than tuned for maximum throughput, because the alternative starves readers under sustained write load, and in my experience that is a worse failure than a slightly slower writer.

Maintained indexes and unique constraints

Register an index on a field and it is kept current as part of the commit itself, not by a background job that can fall behind. A violated unique constraint aborts the whole write, including the counter you incremented three lines earlier.

Schema evolution that refuses to guess

When a field is renamed between releases, ZeroZ DB leaves it unset rather than filling it from some unrelated field that happens to share its type. Renames have to be declared explicitly. This is stricter than the underlying engine's default and it is stricter on purpose, because silently moving data between fields is a corruption that no test catches and no log records.

Ownership safety across JVMs

One JVM owns a store at a time, and this is enforced rather than just documented. On a local filesystem an OS file lock does the job. On a shared volume, where file locks are unreliable, a heartbeat-renewed lease with a fencing epoch does it, so a challenger takes over only after the lease has expired and the displaced owner stops serving within one heartbeat.

03 · Deployment

One API, three topologies.

You write the application code once. Which mode a node runs in is a deployment decision rather than an architectural one, so you can change it later without touching the code.

Mode
What it is
When
Embedded
The store is private to this JVM. There is no socket, one copy of the graph and full transactions.
A single application, or a store per tenant.
Auto-server
The first JVM to open the store owns it and serves it, and later JVMs discover it and become clients. If the owner dies a survivor takes over and in-flight calls are retried.
Several JVMs, no separate database to operate.
Client
Never owns any data. It talks to a dedicated server that does.
A database tier deployed and scaled on its own.

Reads stay local, even on a client.

A client keeps a replica of the graph, and that replica is refreshed the moment the owner commits rather than by polling. Snapshots are swapped atomically, so a reader never observes a half-applied change. A local read never leaves the heap, whereas a remote query crosses a socket, and that difference is why the API keeps the two apart. So keep in mind that when a read has to be current rather than fast, you run it as a query on the owner.

Under the hood

What sits underneath.

Native object persistence

Underneath, EclipseStore persists the object graph natively, so the object-relational mismatch never comes up, i.e. there are no UPDATE statements and no N+1 queries. The in-memory graph is saved explicitly.

Explicit stores, deliberately

A commit persists what you enlisted, and enlisting does not cascade into objects that are already on disk. Yes, that is more work than an ORM's dirty checking, but it is also the reason a commit never walks an object graph you did not ask it to walk.

Pure Java, no native code

There is no embedded native engine, no driver and no separate process. It is a library on your classpath, and in embedded mode it is the only thing between your object and the file.

Limits

What this is not.

A database that does not tell you where it stops is not one you should trust with a system of record, so here is where ZeroZ DB stops.

One writer at a time.

Writes are serialised, so throughput is bounded by that and by fsync rather than by the network. If your bottleneck is concurrent write volume, this is not the database for you and you should look elsewhere.

No SQL and no query language.

Queries are Java streams over maintained indexes. That is by design and not a missing feature, but it does mean that no reporting tool will connect to it, and ad-hoc analytics belong somewhere else.

The graph lives in memory.

A store is bounded by the heap you can give it. Very large or mostly cold datasets are better served by a conventional database, or by lazily loaded sub-graphs if you want to stay here.

The API is still moving.

The transaction, mode and schema APIs are the settled parts, and the guide says which. Pin your version and read the changelog before you upgrade.

How it is verified.

The test suite kills a JVM in the middle of a write and re-opens the store to prove that nothing that was acknowledged has been lost. A multi-process harness runs concurrent clients across real JVMs against invariants (balances that must add up, counters that must not skip, unique indexes that must hold) and then re-opens the store and checks them again. It is published on Maven Central and it is in use in one real application. But it is a young project, and I would rather say so here than have you find out later.

Documentation ↗

The guide, the API, and the limitations.

Maven

<dependency>
    <groupId>com.zeroz4j</groupId>
    <artifactId>zerozdb</artifactId>
</dependency>

latest version on Maven Central ↗