Skip to content

Defining Entities

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

Defining Entities

What this page covers: the EntityDescriptor — the single piece of metadata that tells a backend how to model your entity: its collection name, how to extract the key, how to (de)serialize it, and (later) its indexes and version field. Get this right once and every backend understands your entity.

📌 Note — an EntityDescriptor is immutable and backend-agnostic. You build one per entity type and hand the same descriptor to any storage. Repositories are cached per collection name, so storage.repository(sameDescriptor) returns the same object.


The smallest descriptor

import br.com.finalcraft.everydatabase.EntityDescriptor;
import br.com.finalcraft.everydatabase.codec.JacksonJsonCodec;

import java.util.UUID;

EntityDescriptor<UUID, PlayerData> PLAYERS = EntityDescriptor.builder(UUID.class, PlayerData.class)
        .collection("players")
        .keyExtractor(PlayerData::getUuid)
        .codec(new JacksonJsonCodec<>(PlayerData.class))
        .build();

Four required pieces: the types (in builder), the collection, the keyExtractor, and the codec. build() validates them and fails fast if any is missing or invalid.

⚠️ Gotchabuilder(Class<K> keyType, Class<V> type) takes the key type FIRST. Both parameters are Class<?>, so a flip compiles and only blows up at runtime. The real signature is:

public static <K, V> Builder<K, V> builder(Class<K> keyType, Class<V> type)

Key first (UUID.class), entity second (PlayerData.class).


Builder methods

Method Required? What it sets
builder(keyType, type) the Class<K> key type and Class<V> entity type (key first)
.collection(String) the logical collection/table/directory name (validated — see below)
.keyExtractor(Function<V, K>) how to read an entity's key
.codec(Codec<V>) the serialization strategy
.index(IndexHint) optional a manual secondary index (see Indexing & Queries)
.version(getter, setter) / .versioned() optional optimistic locking (see Optimistic Locking)
.build() validates, scans annotations, returns the immutable descriptor

build() does more than assemble fields: it scans the entity class for @Indexed and @OptimisticLock annotations and merges them in. Those two are covered on Indexing & Queries and Optimistic Locking; this page focuses on the four essentials.


The collection name

The collection is the logical name of the table (SQL), collection (Mongo), or directory (local files). It is validated at build() against:

^[a-zA-Z][a-zA-Z0-9_]*$

Start with a letter; the rest may be letters, digits, or underscores. No spaces, hyphens, dots, backticks, or quotes. This is the safe intersection of identifier rules across every supported backend, so a valid name never needs quoting or escaping anywhere.

.collection("players")        // ✅
.collection("guild_members")  // ✅
.collection("audit_log_2024") // ✅
.collection("guild-members")  // ❌ hyphen  -> IllegalStateException at build()
.collection("2fa")            // ❌ leading digit
.collection("user.profile")   // ❌ dot

A bad name throws IllegalStateException from build() with a message telling you the rule.


What makes a valid key

The key identifies an entity. It is persisted two ways, and a key type must satisfy both:

  • by its toString() — the SQL primary key, the Mongo unique index, the LocalFile filename;
  • by equals / hashCode — the in-memory backend and the manager cache.

So a key type needs a stable, unique toString() of at most 255 characters and value-based equals/hashCode. These all qualify out of the box:

  • UUID, String, Long, Integer, Boolean
  • any record (value-based by definition)

The default identity Object.toString() (e.g. com.example.Foo@1b6d3586) does not — it's neither stable across runs nor value-based.

⚠️ Gotcha — an oversized key is rejected up front, never silently truncated. If a key's toString() exceeds 255 characters, save / saveAll return a future that completes exceptionally with IllegalArgumentException — the key never reaches storage, so it can't be truncated into a collision.

💡 Tip — the keyExtractor just reads the key off an instance; it does not have to be a field. A composite record key works fine:

public record SettingKey(String scope, String name) {}
// ...
.keyExtractor(s -> new SettingKey(s.getScope(), s.getName()))

The whole key contract (the 255-char limit, the dual persistence) is also detailed on Entities, Keys & Collections.


Making the entity serializable

The codec turns your entity into bytes and back. With the default JacksonJsonCodec, your entity must be Jackson-serializable (Jackson 2.x, com.fasterxml.jackson.*). In practice that means:

  • a no-arg constructor (or an appropriate @JsonCreator), and
  • accessors Jackson can use (getters/setters), or Jackson annotations describing the mapping.
public class PlayerData {
    private UUID uuid;
    private String name;
    private int score;

    public PlayerData() {}                                  // <- Jackson needs this
    public PlayerData(UUID uuid, String name, int score) {
        this.uuid = uuid; this.name = name; this.score = score;
    }
    public UUID getUuid()   { return uuid; }
    public String getName() { return name; }
    public int getScore()   { return score; }
    // setters...
}

Lombok works well here — @Data @NoArgsConstructor @AllArgsConstructor gives you exactly the shape above. (The test entities and the manager examples are written this way.)

Choosing a codec

Codec Output Use it for
new JacksonJsonCodec<>(Type.class) compact JSON the default — smallest payload, what a database wants
JacksonJsonCodec.pretty(Type.class) indented JSON human-readable LocalFile output
new JacksonYamlCodec<>(Type.class) YAML LocalFile only — eyeball-friendly .yml files

⚠️ Gotcha — SQL, Mongo, and InMemory require a JSON codec (they parse/store the payload as native JSON). Only LocalFile accepts a non-JSON codec like JacksonYamlCodec. Attaching a YAML codec to a SQL descriptor is rejected. The deciding signal is Codec.isJsonCodec(); the full story — content types, custom codecs — is on Codecs.

💡 TipJacksonJsonCodec.pretty(...) on a LocalFile backend gives you indented JSON files you can read and diff by hand, without leaving JSON for YAML.


Reuse the descriptor everywhere

Because a descriptor is immutable and engine-neutral, the same PLAYERS constant drives every backend — that's what makes the one-line backend swap in Quick Start possible, and what lets Moving Data Between Backends copy a collection from one engine to another by handing the transfer the same descriptor.

Repository<UUID, PlayerData> onSql   = sqlStorage.repository(PLAYERS);
Repository<UUID, PlayerData> onMongo  = mongoStorage.repository(PLAYERS);   // same descriptor

💡 Tip — declare descriptors as static final constants (often in one Descriptors holder class) and pass them around. Don't rebuild a descriptor per call; build once at startup.


See also

Clone this wiki locally