-
Notifications
You must be signed in to change notification settings - Fork 1
Extending the Library
What this page covers: the extension points that are actually meant to be extended — writing a custom
Codec<V> (and the one method you must override when your content-type lacks "json"), reusing the
manager seam (RefRegistry.codec) instead of re-wiring Jackson by hand, and an honest map of where the
library stops being pluggable. Everything here is a real, supported seam; this page deliberately does not
overpromise a backend-plugin SPI that doesn't exist.
📌 Note — most "extension" you'll ever need is composition, not subclassing: a custom
ObjectMapperpassed intoJacksonJsonCodec, a domain-namedCachingManagersubclass, your ownStorageLogSink. Reach for a brand-newCodeconly when JSON/YAML genuinely don't fit.
A Codec<V> converts an entity to/from bytes and names the format. The full SPI is four methods, two of them
defaulted:
package br.com.finalcraft.everydatabase.codec;
public interface Codec<V> {
byte[] encode(V value) throws CodecException; // entity -> bytes
V decode(byte[] data) throws CodecException; // bytes -> entity
String contentType(); // e.g. "application/json"
default boolean isJsonCodec() { return contentType().contains("json"); }
default String fileExtension() { /* derives from contentType() */ }
}Here's a complete codec that stores entities as compact JSON via a custom-configured mapper — the common real-world case (custom date format, a registered module, a naming strategy):
import br.com.finalcraft.everydatabase.codec.Codec;
import br.com.finalcraft.everydatabase.codec.CodecException;
import com.fasterxml.jackson.databind.ObjectMapper;
public final class MyJsonCodec<V> implements Codec<V> {
private final ObjectMapper mapper; // your fully-configured mapper
private final Class<V> type;
public MyJsonCodec(Class<V> type, ObjectMapper mapper) {
this.type = type;
this.mapper = mapper;
}
@Override public byte[] encode(V value) throws CodecException {
try { return mapper.writeValueAsBytes(value); }
catch (Exception e) { throw new CodecException("encode " + type.getSimpleName(), e); }
}
@Override public V decode(byte[] data) throws CodecException {
try { return mapper.readValue(data, type); }
catch (Exception e) { throw new CodecException("decode " + type.getSimpleName(), e); }
}
@Override public String contentType() { return "application/json"; }
// isJsonCodec()/fileExtension() defaults are correct here — "application/json" contains "json".
}Attach it like any built-in codec:
EntityDescriptor<UUID, PlayerData> PLAYERS = EntityDescriptor.builder(UUID.class, PlayerData.class)
.collection("players")
.keyExtractor(PlayerData::getUuid)
.codec(new MyJsonCodec<>(PlayerData.class, myMapper))
.build();💡 Tip — if all you need is a configured Jackson mapper, you don't even need a new class. Both built-ins take one:
new JacksonJsonCodec<>(Type.class, mapper)andnew JacksonYamlCodec<>(Type.class, mapper). Custom serializers, date formats, naming strategies — all of it rides in on the mapper. See Codecs.
Several backends (SQL, MongoDB, InMemory) parse or store the payload as native JSON, so they require a
JSON codec. They decide by calling isJsonCodec(), whose default checks whether contentType() contains
the substring "json":
default boolean isJsonCodec() { return contentType().contains("json"); }That default is correct for "application/json". But if your format is JSON while your content-type
doesn't spell it (a vendor type, say "application/vnd.myapp.v2"), the default returns false and those
backends will reject your codec with an IllegalArgumentException at repository(...) time.
⚠️ Gotcha — overrideisJsonCodec()to returntruewhenever your codec emits JSON but its content-type string lacks the literal"json". Otherwise SQL/Mongo/InMemory refuse it even though the bytes are perfectly valid JSON. Conversely, never returntruefor a non-JSON format — those backends will try to parse it as JSON and fail at runtime. (LocalFile is the only backend that accepts a non-JSON codec; it treats the payload as opaque bytes — which is whyJacksonYamlCodecis LocalFile-only.)
@Override public String contentType() { return "application/vnd.myapp.v2"; }
@Override public boolean isJsonCodec() { return true; } // it IS json; tell the JSON-requiring backends so
@Override public String fileExtension(){ return "json"; } // and give LocalFile a sensible filename📌 Note —
fileExtension()only matters for LocalFile (one file per entity). The default derives it from the content-type (…/json→json,…/yaml→yml, otherwise the last path segment). Override it when the derivation would produce something ugly.
A non-JSON codec (e.g. a binary format) is fine — just keep it on backends that store opaque bytes:
public final class SmileCodec<V> implements Codec<V> {
@Override public String contentType() { return "application/x-jackson-smile"; }
@Override public boolean isJsonCodec() { return false; } // binary — LocalFile only
@Override public String fileExtension(){ return "smile"; }
// encode/decode via a SmileMapper ...
}If your entities carry Ref fields (the Caching & References add-on), they need a codec that knows how
to write a Ref as its key and recover the target type on read. :core can't depend on :manager, so the
ref-awareness can't be baked into core's codecs — the manager module provides the thin seam.
The idiomatic path is RefRegistry.codec(Type.class), which returns a JacksonJsonCodec whose mapper
already has the ref module bound to that registry:
RefRegistry refRegistry = new RefRegistry();
EntityDescriptor<UUID, Player> PLAYERS = EntityDescriptor.builder(UUID.class, Player.class)
.collection("players")
.keyExtractor(Player::getUuid)
.codec(refRegistry.codec(Player.class)) // ref-aware codec bound to 'refRegistry'
.build();If you maintain your own ObjectMapper and want it ref-aware (custom serializers and Ref support),
compose the module yourself — this is the supported extension point:
import br.com.finalcraft.everydatabase.manager.jackson.RefModule;
import br.com.finalcraft.everydatabase.manager.jackson.RefCodecs;
import com.fasterxml.jackson.databind.ObjectMapper;
// (a) register the module onto an existing mapper:
ObjectMapper mine = new ObjectMapper()
.registerModule(myDomainModule)
.registerModule(new RefModule(refRegistry)); // <- binds Refs read by this mapper to 'refRegistry'
Codec<Player> codec = new JacksonJsonCodec<>(Player.class, mine);
// (b) or let RefCodecs build a fresh bound mapper for you:
ObjectMapper bound = RefCodecs.newMapper(refRegistry);
⚠️ Gotcha — aRefread through a codec is bound to that codec's registry. A bareRef.of(key, type, null)(the explicit way to make an unbound ref, with anullregistry) is fine to store (it serializes as just the key) butresolve()/peek()fail fast. If you build a custom mapper and forget theRefModule, your refs round-trip as unbound and won't resolve. See Caching & References.
A CachingManager<K,V> is meant to be subclassed for a readable domain name and a baked-in cache policy — pass
the registry up to super(...):
public final class GuildManager extends CachingManager<UUID, Guild> {
public GuildManager(Storage storage, RefRegistry registry) {
super(GUILDS, storage, CacheOptions.builder()
.policy(CachePolicy.always())
.maxSize(1000)
.build(), registry); // self-registers in 'registry' for Guild
}
}For most needs you configure through CacheOptions and CachePolicy and never touch the internals.
But the cache layer is now deliberately open for extension: the manager.cache classes
(CacheEntry, CacheOptions, CachePolicy, LruCacheStore) are non-final with protected
members, and CachingManager itself is non-final and exposes its key fields (store, repository,
options, stampGen, …) as protected. So a deployment can subclass — for example, to add cache
metrics, or to swap LruCacheStore's backing map for a lock-striped one (e.g. Caffeine):
public class MeteredLruCacheStore<K, V> extends LruCacheStore<K, V> {
public MeteredLruCacheStore(int maxSize) { super(maxSize); }
@Override public CacheEntry<V> get(K key) {
CacheEntry<V> e = super.get(key);
metrics.record(e == null ? "miss" : "hit");
return e;
}
}
⚠️ Gotcha — the cell/stamp/tombstone model is load-bearing for the concurrency guarantees (a monotonic stamp is what stops a slow reload regressing a newer write or resurrecting a newer delete). Subclass to observe or swap the backing map, but preserve the stamp ordering of the compound operations (installColdMiss,tombstone,markStale) or you reintroduce the races they exist to prevent. PreferCacheOptions/CachePolicywhenever it suffices. See Caching Managers and Cache Policies & Freshness.
Routing log events somewhere custom is a first-class seam — install a sink once and the whole library uses it:
StorageLogSinks.installDefault(event -> myLogger.info(event.format()));The default resolves per event: a host sink installed via installDefault wins, else SLF4J if present, else a
no-op. A throwing sink never breaks a storage operation. Full surface on Logging & Diagnostics.
Be honest about the boundary so you don't build against something that isn't there:
| Want to… | Supported? | Do this instead |
|---|---|---|
| Add a new codec format | ✅ | implement Codec<V> (this page) |
| Customize JSON/YAML | ✅ | pass your own ObjectMapper to the built-in codecs |
| Make a custom mapper ref-aware | ✅ |
new RefModule(registry) / RefCodecs.newMapper(registry)
|
| Name + policy a cache per type | ✅ | subclass CachingManager
|
| Add cache metrics / swap the backing map | ✅ | subclass manager.cache classes (non-final, protected) — preserve the stamp ordering |
| Invalidate caches across instances | ✅ | wire CacheSync — see Cross-Process Cache Sync
|
| Route logs anywhere | ✅ | StorageLogSinks.installDefault(...) |
Add a new IndexHint.fieldType
|
❌ | use the existing types (string/integer/bigInt/decimal/bool/timestamp) — see Indexing & Queries
|
Add native OR to Query
|
❌ | union two queries client-side — see Indexing & Queries |
| Write a brand-new backend module | not a stable published SPI. The modules/* packages (sql, mongo, localfile, memory) are internal; subclassing SqlStorage for a new dialect works but is unversioned territory — open an issue first. |
🧭 Decision — need a new SQL dialect? The existing pattern is a thin subclass of
SqlStorageoverriding identifier quoting (q()), column types, and upsert SQL (that's howPostgreSqlStorageandH2SqlStorageare built). It's doable, but those classes are internal and may change between versions — treat it as a contribution to the project rather than a public API you build a product on. For a genuinely new engine (not SQL), preferStorageTransferto bridge data and raise the request upstream.
-
Codecs — the built-in
JacksonJsonCodec(compact /pretty) andJacksonYamlCodec, andisJsonCodecas the backend seam. -
Defining Entities — attaching a codec to an
EntityDescriptor. -
Caching & References — the manager add-on and why
RefCodecslives in its own module. -
Logging & Diagnostics — installing a custom
StorageLogSink. - Indexing & Queries — the fixed index-type and query surface.
- Design & Internals — the idioms (capabilities-as-interfaces, no global registry) these seams follow.
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