ZeroZ DB · The zero-impedance database
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.
The cost
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.
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 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.
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
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.
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
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.
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.
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.
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.
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.
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.
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
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.
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
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.
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.
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
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.
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.
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.
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 transaction, mode and schema APIs are the settled parts, and the guide says which. Pin your version and read the changelog before you upgrade.
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.
The guide, the API, and the limitations.
Maven
<dependency> <groupId>com.zeroz4j</groupId> <artifactId>zerozdb</artifactId> </dependency>