-
Notifications
You must be signed in to change notification settings - Fork 1
Architecture Overview
What this page covers: the five types you actually touch — EntityDescriptor, Codec,
Storages, Storage, Repository — how they fit together, and the one idiom that runs through
the whole library: capabilities are interfaces you instanceof-check, not flags you read.
import br.com.finalcraft.everydatabase.*;
import br.com.finalcraft.everydatabase.codec.JacksonJsonCodec;
import br.com.finalcraft.everydatabase.modules.sql.SqlConfig;
// 1. Describe the entity once: key type FIRST, entity type second.
EntityDescriptor<UUID, PlayerData> PLAYERS = EntityDescriptor.builder(UUID.class, PlayerData.class)
.collection("players")
.keyExtractor(PlayerData::getUuid)
.codec(new JacksonJsonCodec<>(PlayerData.class))
.build();
// 2. Pick a backend (a Storage).
Storage storage = Storages.createSQL(new SqlConfig("jdbc:mariadb://localhost:3306/mydb", "root", "root"));
storage.init().join();
// 3. Get a typed Repository and do CRUD. Everything is async.
Repository<UUID, PlayerData> repo = storage.repository(PLAYERS);
repo.save(new PlayerData(aliceId, "Alice", 100)).join();
Optional<PlayerData> alice = repo.find(aliceId).join();
storage.close().join();That is the entire surface for plain persistence. Swapping createSQL(...) for createMongo(...)
or createH2(...) changes nothing below storage.repository(...). See Quick Start for the
full runnable version and Choosing a Backend for picking one.
📌 Note — every I/O call returns a
CompletableFuture. These examples use.join()for brevity, but you normally compose withthenApply/thenCompose. There are no blocking variants. See The Async API.
| Type | Role | You hold it… |
|---|---|---|
EntityDescriptor<K, V> |
Immutable metadata: collection name, key/entity types, keyExtractor, codec, index hints, optional version accessors. |
once per entity type, as a static final constant |
Codec<V> |
The serialization strategy (entity ↔ bytes + a content type). Attached to the descriptor. | inside the descriptor |
Storages |
The static factory. Typed builders (createSQL, createMongo, …) return concrete types; the generic create(StorageConfig) dispatches by config type. |
call once at startup |
Storage |
A live backend: lifecycle (init/close/health) and a factory for repositories. |
one per backend, app-lifetime |
Repository<K, V> |
Typed CRUD for one collection: find/findMany/save/saveAll/delete/exists/count/all, plus index reads findBy/query. |
cached per descriptor by the storage |
The data flows in exactly that order: describe → build a storage → ask it for a repository → operate. The descriptor is the contract; the storage is the connection; the repository is the typed door into one collection.
flowchart LR
ED["EntityDescriptor<K,V><br/>collection · keyExtractor · codec · indexes"]
C["Codec<V><br/>encode / decode"]
F["Storages<br/>(factory)"]
S["Storage<br/>init / close / health"]
R["Repository<K,V><br/>find / save / query …"]
DB[("backend<br/>SQL · Mongo · File · …")]
C -->|attached to| ED
F -->|create*| S
ED -->|repository(descriptor)| S
S -->|repository(d)| R
R <-->|async I/O| DB
public interface Storage {
CompletableFuture<Void> init(); // idempotent
CompletableFuture<Void> close(); // idempotent
CompletableFuture<HealthStatus> health(); // fast ping
<K, V> Repository<K, V> repository(EntityDescriptor<K, V> descriptor);
StorageLogConfig getStorageLogConfig();
Storage setStorageLogConfig(StorageLogConfig config);
}repository(...) is the only non-async method here — it returns the typed door synchronously (it
opens no connection by itself). A storage caches repositories per descriptor: calling
storage.repository(PLAYERS) twice with the same descriptor returns the same object.
📌 Note —
init,close, andhealthare all idempotent. Callinginit()twice is safe;close()on an already-closed storage is a no-op.
CompletableFuture<Optional<V>> find(K key); // empty if absent
CompletableFuture<List<V>> findMany(Collection<K> keys); // missing keys omitted
CompletableFuture<Void> save(V entity); // upsert
CompletableFuture<Void> saveAll(Collection<V> entities);
CompletableFuture<Boolean> delete(K key); // true if it existed
CompletableFuture<Boolean> exists(K key);
CompletableFuture<Long> count();
CompletableFuture<Stream<V>> all();
CompletableFuture<List<V>> findBy(String fieldPath, Object value); // indexed
CompletableFuture<List<V>> query(Query query); // indexed, compositesave/saveAll are upserts (insert or replace). findMany silently omits keys that aren't
present — the result list is never padded with nulls. The findBy/query pair only works on
fields you declared as index hints; see Indexing & Queries.
These three are covered in depth on their own pages — they're listed here so you can see where they sit in the flow:
-
EntityDescriptoris built withEntityDescriptor.builder(KeyType.class, EntityType.class). See Entities, Keys & Collections and Defining Entities. -
Codecis the serialization seam;JacksonJsonCodecis the default. See Codecs. -
Storagesis the factory; prefer the typed builders. See the gotcha below.
⚠️ Gotcha —EntityDescriptor.builder(...)takes the key type first, entity type second. Both areClassobjects, so flipping them compiles but produces a descriptor with<K, V>reversed — caught only when the generics no longer line up at therepository(...)call. Read it as "a descriptor fromUUIDtoPlayerData".
This is the central design idiom. Optional features are extra interfaces a Storage may
implement. You discover them with instanceof, not by reading a boolean — so the type system tells
you, per backend, what's available, and a missing capability is a compile-time absence rather than a
runtime exception.
Storage storage = Storages.createSQL(sqlConfig);
// Transactions? Only if this backend implements TransactionalStorage.
if (storage instanceof TransactionalStorage tx) {
tx.inTransaction(scope -> {
Repository<UUID, Account> accounts = scope.repository(ACCOUNTS);
return accounts.save(debited).thenCompose(v -> accounts.save(credited));
}).join();
}
// Migrations? Only if this backend implements SchemaAwareStorage.
if (storage instanceof SchemaAwareStorage schema) {
schema.register(new AddEmailColumn()).migrate().join();
}The two core capabilities:
| Capability interface | Adds | Supported by | Not by |
|---|---|---|---|
tx.TransactionalStorage |
inTransaction(scope -> future); repositories from the TransactionScope share one connection, commit on success, roll back on exception or scope.rollback()
|
SQL (incl. H2), Mongo (replica set required), InMemory (no isolation) | LocalFile |
schema.SchemaAwareStorage |
register(migrations…).migrate(); forward-only, applied versions tracked in a reserved _schema_migrations table/collection/file |
SQL, Mongo, LocalFile | InMemory |
The payoff of typed builders: Storages.createSQL(...) returns SqlStorage (which is a
TransactionalStorage and a SchemaAwareStorage), so when you know the backend at compile time you
can skip the instanceof entirely and call inTransaction directly. Reach for instanceof when
the backend is chosen at runtime from config.
🧭 Decision — use the typed builder (
createSQL/createPostgreSQL/createH2/createMongo/createLocalFile/createInMemory) when the backend is known at compile time: you get the concrete type and its capabilities without a cast. Use the genericStorages.create(StorageConfig)when the backend is selected at runtime — theninstanceof-check for the capabilities you need.
⚠️ Gotcha — the genericStorages.create(StorageConfig)always picks the MySQL/MariaDB dialect for anySqlConfig— it can't tell PostgreSQL/H2 apart from the config alone (the dialect lives in theStoragesubclass, not the config). UsecreatePostgreSQL/createH2when you need those dialects.
Deeper dives on each capability: Transactions and Schema Migrations. Other features follow the same pattern — optimistic locking is opted into per descriptor (Optimistic Locking), and cross-backend copying is a standalone tool (Moving Data Between Backends).
-
Core (
everydatabase-core) is everything above: the contract types, the six backends, codecs, indexing, transactions, migrations, transfer, and logging. It's the recommended flavor. -
The manager add-on (
everydatabase-manager) is an optional feature layer that sits in front of:core: typed references (Ref) and per-type caching managers. It's not a backend and not a capability interface — it's a separate artifact you opt into. See Caching & References. - Distribution flavors (core / standalone / libby) differ only in how the dependencies are packaged, not in the API. See Distribution Flavors.
- Quick Start — the full runnable end-to-end example.
-
The Async API — the
CompletableFuturemodel every call uses. - Entities, Keys & Collections — the key contract and collection rules.
- Codecs — the serialization seam.
- Defining Entities — building a descriptor in detail.
- Choosing a Backend — the capability matrix and decision guide.
- Transactions · Schema Migrations — the two capability interfaces in depth.
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