Skip to content

Architecture Overview

Petrus Pradella edited this page Jun 23, 2026 · 4 revisions

Architecture Overview

What this page covers: the five types you actually touch, how they fit together, and the central idiom — capabilities are interfaces you instanceof-check, not flags.


The 30-second version

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.
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();

Swapping createSQL(...) for createMongo(...) or createH2(...) changes nothing below storage.repository(...). See Quick Start for the full runnable version.

📌 Every I/O call returns a CompletableFuture. These examples use .join() for brevity; there are no blocking variants. See The Async API.


The five types

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). 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) + a factory for repositories. repository(...) is the only synchronous call; it caches per descriptor. one per backend, app-lifetime
Repository<K, V> Typed CRUD: find/findMany/save/saveAll/delete/exists/count/all, plus index reads findBy/query. cached per descriptor by the storage

Data flows in order: describe → build a storage → ask it for a repository → operate.

flowchart LR
    ED["EntityDescriptor&lt;K,V&gt;<br/>collection · keyExtractor · codec · indexes"]
    C["Codec&lt;V&gt;<br/>encode / decode"]
    F["Storages<br/>(factory)"]
    S["Storage<br/>init / close / health"]
    R["Repository&lt;K,V&gt;<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
Loading

⚠️ GotchaEntityDescriptor.builder(...) takes the key type first, entity type second. Both are Class objects, so flipping them compiles but reverses <K, V> — only caught when the generics don't line up at the repository(...) call.

For the full CRUD surface see CRUD Operations; for filtered reads see Indexing & Queries. Defining Entities covers building descriptors and Codecs covers the serialization seam.


Capabilities are interfaces, not flags

Optional features are extra interfaces a Storage may implement — discover them with instanceof, not a boolean flag:

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();
}
if (storage instanceof SchemaAwareStorage schema) {
    schema.register(new AddEmailColumn()).migrate().join();
}
Capability Adds Supported by Not by
TransactionalStorage inTransaction(scope → future); scope repositories share one connection, commit on success, roll back on exception or scope.rollback(). SQL (incl. H2), Mongo (replica set required), InMemory (no isolation) LocalFile
SchemaAwareStorage register(migrations…).migrate(); forward-only, versions tracked in a reserved _schema_migrations table/collection/file. SQL, Mongo, LocalFile InMemory
ChangeFeedStorage subscribe(listener) + originId(); a backend-native push feed so other instances can invalidate their caches. Wired via the manager's CacheSync. Mongo (Change Streams), PostgreSQL (LISTEN/NOTIFY), InMemory (local writes) MySQL/MariaDB, H2, LocalFile (use polling)

Typed builders (createSQL, createPostgreSQL, etc.) return the concrete type, so when the backend is known at compile time you can call inTransaction directly without a cast.

⚠️ GotchaStorages.create(StorageConfig) always picks the MySQL/MariaDB dialect for any SqlConfig. Use createPostgreSQL / createH2 when you need those dialects.


What lives where

  • Core (everydatabase-core) — the contract types, six backends, codecs, indexing, transactions, migrations, transfer, and logging. The recommended flavor.
  • Manager add-on (everydatabase-manager) — typed references (Ref) and per-type caching managers, sitting in front of :core. See Caching & References.
  • Distribution flavors (core / libby) — differ only in how dependencies are packaged, not in the API. See Distribution Flavors.

See also

Clone this wiki locally