Skip to content

In Memory

Petrus Pradella edited this page Jul 5, 2026 · 5 revisions

In-Memory

What this page covers: the ephemeral in-memory backend — Storages.createInMemory() (the no-arg form you almost always want), why it's the go-to for tests, CI, and prototyping, the fact that data lives only as long as the JVM, and its one deliberate non-property: transactions exist but provide no isolation. Migrations are supported (with an ephemeral ledger). Everything else (CRUD, queries, indexing) is identical to every other backend.

📌 Note — because the API is identical across backends, an in-memory store is a faithful stand-in for a real database in tests. Write your code against Storage / Repository, run it against in-memory in CI, and ship it on SQL/Mongo — the data-access code never changes. See Choosing a Backend.


The 30-second version

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

import java.util.Optional;
import java.util.UUID;

// 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))       // in-memory requires a JSON codec
        .build();

// 2. Open it — no config, no server, no setup.
InMemoryStorage storage = Storages.createInMemory();
storage.init().join();

// 3. Use it — exactly like any other backend.
Repository<UUID, PlayerData> repo = storage.repository(PLAYERS);
UUID id = UUID.randomUUID();
repo.save(new PlayerData(id, "Alice", 100)).join();           // upsert
Optional<PlayerData> alice = repo.find(id).join();            // -> Optional[Alice]

storage.close().join();                                        // clears the store

createInMemory() returns the concrete InMemoryStorage type. The PlayerData entity is the same plain Jackson POJO used throughout the wiki (Quick Start).

📌 Note — every I/O call returns a CompletableFuture. In-memory operations complete immediately on the calling thread, but the contract is the same async surface as every other backend — .join() for brevity, compose with thenApply / thenCompose, no blocking variants. See The Async API.


Construction: prefer the no-arg form

InMemoryStorage mem = Storages.createInMemory();              // preferred

There is no configuration to give — nothing persists, so there is nothing to point it at.

⚠️ Gotcha — an InMemoryConfig overload exists (Storages.createInMemory(new InMemoryConfig())) only so the generic Storages.create(StorageConfig) can dispatch a runtime-selected config here. It ignores its argumentInMemoryConfig carries no fields. Prefer the no-arg createInMemory(); reach for the overload only when a runtime-selected StorageConfig flows through create(...).

// Runtime-selected backend: in-memory is just one of the cases create(...) dispatches.
StorageConfig config = loadConfig();          // may be SqlConfig / MongoConfig / InMemoryConfig / ...
Storage storage = Storages.create(config);    // an InMemoryConfig lands here, fields-and-all ignored

Ephemeral by design

Data lives in JVM heap and disappears when the process exits or you call close() (which clears the store). There is nothing on disk and nothing to back up.

The entity is held as a live JVM object (its codec-parsed form), so reads are instant. Secondary indexes are kept as an in-memory Map<value, Set<key>>, so indexed queries are real lookups, not scans:

EntityDescriptor<UUID, PlayerData> PLAYERS = EntityDescriptor.builder(UUID.class, PlayerData.class)
        .collection("players")
        .keyExtractor(PlayerData::getUuid)
        .index(IndexHint.integer("score"))           // backed by an in-memory map
        .codec(new JacksonJsonCodec<>(PlayerData.class))
        .build();

repo.findBy("score", 100).join();
repo.query(Query.range("score", 50, null)).join();   // score >= 50 (inclusive, null = open end)

Querying an undeclared field throws IllegalArgumentException, the same as on every backend, so test code exercises the same declaration rules as production. See Indexing & Queries.

⚠️ Gotcha — in-memory requires a JSON codec (codec.isJsonCodec()); repository(...) throws IllegalArgumentException for a non-JSON codec like JacksonYamlCodec (which only the file backends, Local Files and Grouped Files, accept). The key contract is the usual one — a stable, unique toString() of ≤ 255 characters. See Codecs and Entities, Keys & Collections.


Transactions: present, but no isolation

InMemoryStorage implements tx.TransactionalStorage, so code written against the transaction API runs against in-memory storage — useful for testing transactional logic without a real database:

storage.inTransaction(scope -> {
    Repository<UUID, PlayerData> txRepo = scope.repository(PLAYERS);
    return txRepo.save(alice)
            .thenCompose(__ -> txRepo.save(bob));
}).join();

⚠️ Gotcha — there is no real isolation. Writes inside the transaction are visible to concurrent readers immediately, and scope.rollback() is a no-op (nothing is buffered to undo). The interface contract (commit/rollback completes) is honoured so the shape of your code works, but in-memory must not be used to test rollback semantics or isolation — only that transactional code paths execute. For real ACID in a test, use H2 (full SQL transactions, embedded, no server). See Transactions.


Migrations: supported, with an ephemeral ledger

InMemoryStorage does implement schema.SchemaAwareStorage, so storage.register(...).migrate() works. The catch: its ledger of applied versions lives in memory and dies with the instance — a fresh InMemoryStorage starts at SchemaVersion.none() and re-applies every migration. That's correct (data and ledger reset together), but it makes DDL-style migrations meaningless here (entities are JSON blobs in a map, not rows). The point of an in-memory migration is a data migration: seed reference data, or transform existing entities. Extend InMemoryMigration and override executeOnStorage(InMemoryStorage):

import br.com.finalcraft.everydatabase.modules.memory.InMemoryMigration;

class V1_SeedDefaults extends InMemoryMigration {
    @Override public String version()     { return "001"; }
    @Override public String description() { return "seed default players"; }
    @Override protected void executeOnStorage(InMemoryStorage storage) {
        storage.repository(PLAYERS).saveAll(DEFAULT_PLAYERS).join();
    }
}

storage.register(new V1_SeedDefaults()).migrate().join();

📌 Note — because the ledger is ephemeral, in-memory is not the place to test that a migration is applied once and only once across restarts — for a persistent _schema_migrations ledger, run that test against H2. See Schema Migrations and the matrix in Choosing a Backend.

Optimistic locking isn't enforced either (in-memory and local files don't enforce it — see Optimistic Locking).


Change feed: the reference push implementation

InMemoryStorage implements changefeed.ChangeFeedStorage — it emits a ChangeEvent on every local write. Cross-process sync is moot here (data is per-process), but this is the reference implementation of the push capability, which makes Cross-Process Cache Sync fully testable without Docker. In a single-process test, pair it with .includeOwnOrigin() so the writer's own events still fan out to a second manager. See Cross-Process Cache Sync.


When to pick in-memory

🧭 Decision — choose in-memory for tests, CI, and prototyping: instant, ephemeral, zero setup, no external service. It exercises the same Storage / Repository API, the same index/query rules, and the same codec requirements as production — so it's a faithful fast stand-in. Do not rely on it for transaction isolation (it has none), nor to test that a migration persists across restarts (its ledger is ephemeral); when a test needs either, reach for H2 (embedded, full SQL features, still no server). Never use in-memory for data you need to keep — it vanishes with the JVM.


See also

  • Choosing a Backend — the capability matrix and where in-memory fits.
  • Quick Start — the minimal describe → open → use → close round-trip.
  • The Async API — the CompletableFuture model every call shares.
  • H2 — the embedded alternative when a test needs real isolation or a persistent migration ledger.
  • Transactions — what "no isolation" means and which backends provide real isolation.
  • Schema MigrationsInMemoryMigration, the ephemeral ledger, and data-migration use.
  • Cross-Process Cache Sync — in-memory is the reference push feed that makes it testable.
  • Indexing & Queries — the index/query rules in-memory shares with every backend.
  • Codecs — why in-memory requires a JSON codec.
  • Running the Tests — how the suite uses embedded backends (no Docker) for fast runs.

Clone this wiki locally