Skip to content

In Memory

Petrus Pradella edited this page Jun 23, 2026 · 3 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 two deliberate non-properties: transactions exist but provide no isolation, and there are no migrations. 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 — data exists only while the JVM runs, 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 to this backend. It ignores its argumentInMemoryConfig carries no fields. Prefer the no-arg createInMemory(); reach for the config 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 Local Files accepts). 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 (there's nothing 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 behavior in a test, use H2 (full SQL transactions, embedded, no server). See Transactions.


No migrations

InMemoryStorage does not implement schema.SchemaAwareStorage — there is nothing to migrate in an ephemeral store, and no _schema_migrations tracking. Calling migration methods isn't part of this backend's surface.

📌 Note — if your code calls storage.register(...).migrate() and you want it to exercise the migration path in a test, run that test against H2 (which implements SchemaAwareStorage) rather than in-memory. 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 does implement changefeed.ChangeFeedStorage — it emits a ChangeEvent on every local write. Cross-process sync is moot here (the data is per-process), but this is the reference implementation of the push capability, which is what 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 or migrations (it has neither); when a test needs those, 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 transactions or migrations.
  • Transactions — what "no isolation" means and which backends provide real isolation.
  • Schema Migrations — why in-memory has none.
  • 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