-
Notifications
You must be signed in to change notification settings - Fork 1
Design and Internals
What this page covers: the design rationale behind the load-bearing parts of the library, plus the two cross-cutting idioms that explain most of the codebase once you've seen them — capabilities are interfaces, not flags and there is no global registry. This is the page to read before changing something that "looks redundant" — it usually isn't. The wiki teaches; the Javadoc specifies.
📌 Note — this page is the canonical home for why a design is the way it is (and the hazard analysis behind it). The Javadoc owns the exact per-member contract; the in-repo READMEs (
README.md,manager/README.md) carry the practical narrative next to the code.
Each slice of the library has a self-contained decision behind it. The rationale lives next to the
implementation it governs — the project README.md, the manager/README.md for the refs/caching add-on, and
the extensive Javadoc — and the highlights are summarised here.
| Subsystem | Status | Why it's built this way |
|---|---|---|
Refs & managers (manager/, manager/README.md) |
Implemented | The everydatabase-manager add-on: why refs and caching are a separate façade module, the three decoupled concerns (serialization / type recovery / resolution), the cell + monotonic-stamp + tombstone model, and the per-context RefRegistry (no global registry). |
Distribution modules (core/build.gradle, libby/build.gradle) |
Binding | The two distribution flavors (core / libby): the dependency-scope decisions, the GPL MySQL-driver handling (POM metadata in core, runtime download in libby — never bundled), the slf4j compileOnly decision, and why libby's POM excludes core's transitive set. |
Storage logging (log/ package) |
Binding | The logging subsystem: the silent-by-default policy with an unsuppressible ERROR floor, the topic/level model, the privacy defaults (counts/durations, never content), the sink-resolution order, and the implementation checklist of which ops emit. |
Cache sync (changefeed, manager.sync, specs/SPEC_cache_sync.md) |
Implemented | Cross-process cache invalidation: why a capability (ChangeFeedStorage) not a flag, the push (Mongo Change Streams / Postgres LISTEN/NOTIFY) vs pull (PollingCacheSync over Repository.versions) split per backend, why a late/duplicate/dropped event is safe by construction (the cell stamp guard), and why the consumer lives in :manager (where the cache is) while the capability lives in :core. |
| This wiki | Proposed | This wiki itself: taxonomy, page inventory, the source-of-truth hierarchy (README sells, wiki teaches, Javadoc specifies), and the callout/code/language conventions every page follows. |
📌 Note — read each rationale next to the implementation it governs: the distribution decisions next to
core/build.gradleandlibby/build.gradle, the logging decisions next to thelog/package, and the refs/caching rationale next tomanager/README.md.
The central design idiom. Optional features are expressed as extra interfaces a Storage may implement,
discovered with instanceof — never as a boolean you have to trust. The compiler stops you from using a
capability a backend doesn't have.
if (storage instanceof TransactionalStorage tx) {
tx.inTransaction(scope -> { /* ... */ }); // only reachable on backends that actually support it
}
if (storage instanceof SchemaAwareStorage schema) {
schema.register(new V001_Init()).migrate().join();
}The capability interfaces:
| Interface | Adds | Backends that implement it |
|---|---|---|
tx.TransactionalStorage |
inTransaction(scope -> future) |
SQL (incl. H2), Mongo (replica set), InMemory (no isolation) — not LocalFile |
schema.SchemaAwareStorage |
register(...).migrate(), forward-only, _schema_migrations
|
SQL, Mongo, LocalFile — not InMemory |
changefeed.ChangeFeedStorage |
subscribe(listener) + originId(), a push feed for cross-process cache invalidation |
Mongo (Change Streams), PostgreSQL (LISTEN/NOTIFY), InMemory — not MySQL/MariaDB, H2, LocalFile (they poll) |
🧭 Decision — a flag (
storage.supportsTransactions()) would let a backend claim a feature it can't honor, deferring the failure to runtime. An interface makes "no backend pretends to support something it can't" a compile-time fact. Preserve this idiom when adding features: a new optional capability is a new interface a subset of backends implements, not a method onStoragethat throwsUnsupportedOperationException. See Architecture Overview, Transactions, Schema Migrations.
A related expression of the same principle: optimistic locking is enforced only by the backends that
genuinely can (MySQL/MariaDB, PostgreSQL, Mongo). H2 opts out explicitly
(H2SqlRepository.supportsVersioning() returns false) and silently degrades to plain upsert — documented and
tested, never a silent lie. See Optimistic Locking.
The manager add-on used to be tempted toward a process-wide static Class<?> → resolver map. It deliberately
doesn't have one. A RefRegistry is a per-context instance (new RefRegistry()), and a Ref resolves
only against the registry it was bound to.
RefRegistry survival = new RefRegistry();
RefRegistry lobby = new RefRegistry();
survival.manager(heroDesc, survivalStore, CachePolicy.always()); // Hero -> survivalStore
lobby.manager(heroDesc, lobbyStore, CachePolicy.always()); // Hero -> lobbyStore — no collision🧭 Decision — a static default would let two independent contexts (two plugins) register a manager for the same entity type and silently collide (last-writer-wins), discovered only in production. Making the registry per-context turns that silent collision into a thing that can't happen: two registries can each own a
Heromanager on different storages. The registry vends what a ref-aware setup needs (codec(Type.class),manager(...),ref(key, Type.class)) so you can't half-wire it. A bareRef.of(key, type, null)(the explicit unbound form, with anullregistry) fails fast on resolve — never resolves against "some default". See Caching & References.
This is the same instinct as Idiom 1: make the failure mode impossible by construction rather than something you document and hope nobody hits.
A few load-bearing decisions that surprise people reading the source:
-
Everything is async; there are no blocking variants. Every I/O method returns
CompletableFuture; callers use.join()or compose. This isn't an oversight — a sync overload would double the surface and invite blocking on the wrong thread. See The Async API. -
Keys persist by
toString()(≤ 255 chars), match byequals/hashCode. The 255 cap is the safe intersection across all backends; an oversized key makessavecomplete exceptionally rather than silently truncate into a collision. See Entities, Keys & Collections. -
Collection names are validated against
^[a-zA-Z][a-zA-Z0-9_]*$— the intersection of identifier rules across every backend, so no quoting is ever needed. -
The cache cell/stamp/tombstone model (manager module) makes concurrent writes and reloads safe: writes
swap the value in place under a monotonic stamp so a slow reload can't regress a newer write nor resurrect
a newer delete. These internals now live in the
manager.cachepackage and are open for extension — the classes (CacheEntry,CacheOptions,CachePolicy,LruCacheStore) are non-final withprotectedmembers, andCachingManageritself is subclassable — so a deployment can add metrics or swap the backing map. Most code configures viaCacheOptions/CachePolicyand never subclasses. Detailed inmanager/README.md; surfaced in Caching Managers and Extending the Library. -
The dual-target build. Production code is authored in Java 17 syntax but compiled to Java 8 bytecode
via Jabel (
--release 8), while the toolchain that runs the tests is JDK 25. So production code must stay Java-8-runtime-safe (no Java 9+ APIs) even though it reads like modern Java — e.g.StorageExecutorsreflectsnewVirtualThreadPerTaskExecutorinstead of calling it directly. See Building from Source. -
Logging is silent by default with an ERROR floor. Routine ops emit nothing under the factory default;
failures always emit, and no configuration can switch that floor off. Privacy is the default: events carry
counts/durations/collection names, never entity content. Rationale in the
log/package Javadoc; usage in Logging & Diagnostics.
When you're unsure which document to trust, this is the order:
README sells and starts · the wiki teaches · the Javadoc specifies.
A fact lives in exactly one canonical place; everywhere else links to it. Version numbers live in the
version catalog and are surfaced on a single page — Dependency Versions & Overrides — and never restated
elsewhere. Wiki code examples mirror tested entity shapes (TestPlayer, PlayerProfile, …) so they can't
silently rot.
-
Architecture Overview — the
Storage/Repository/EntityDescriptor/Codec/Storagessurface and the capability idiom in context. -
The Async API — the
CompletableFuturemodel that pervades the whole library. - Distribution Flavors — the flavor and GPL/MySQL decisions and their rationale.
- Logging & Diagnostics — the logging subsystem and its rationale.
-
Caching & References — the
everydatabase-manageradd-on and its cell/stamp model. - Cross-Process Cache Sync — the change-feed capability and the push/poll consumer.
- Extending the Library — the seams that are meant to be extended.
- Editing this Wiki — the conventions every page on this site follows.
EveryDatabase · Home · made by Petrus Pradella
Getting Started
Core Concepts
Working with Data
Backends
Manager Module
- Caching & References
- Typed References (Ref)
- Caching Managers
- Cache Policies & Freshness
- Cross-Process Cache Sync
- One Entity, Many Databases
Operations
Advanced
Reference
Contributing