Skip to content

Defining Entities

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

Defining Entities

The EntityDescriptor is: a 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, PlayerEntity> PLAYER_DESCRIPTOR = EntityDescriptor.builder(UUID.class, PlayerEntity.class)
        .collection("players")
        .keyExtractor(PlayerEntity::getUuid)
        .codec(new JacksonJsonCodec<>(PlayerEntity.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.


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

⚠️ 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 PlayerEntity {
    private UUID uuid;
    private String name;
    private int score;

    public PlayerEntity() {}                                  // <- Jackson needs this
    public PlayerEntity(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 PLAYER_DESCRIPTOR 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(PLAYER_DESCRIPTOR);
Repository<UUID, PlayerData> onMongo  = mongoStorage.repository(PLAYER_DESCRIPTOR);   // 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