In-memory journal
InMemoryJournal stores every event in process memory. It’s the
default when you create a system without configuring
persistence — fast, zero setup, perfect for tests and dev.
import { InMemoryJournal, InMemorySnapshotStore, PersistenceExtensionId, ActorSystem } from 'actor-ts';
const system = ActorSystem.create('demo');system.extension(PersistenceExtensionId).configure({ journal: new InMemoryJournal(), snapshotStore: new InMemorySnapshotStore(),});
// Now `PersistentActor`s in this system write to the in-memory journal.It’s also the reference semantics every other journal implementation must match — append-only, monotonic sequence numbers, exact-once delivery to subscribers, concurrency check on expected sequence.
What it does
Section titled “What it does”The journal contract:
interface Journal { append<E>(pid: string, events: E[], expectedSeq: number, tags?: string[]): Promise<PersistentEvent<E>[]>; read<E>(pid: string, fromSeq: number, toSeq?: number): Promise<PersistentEvent<E>[]>; highestSeq(pid: string): Promise<number>; deleteEventsUpTo(pid: string, seqNr: number): Promise<void>; persistenceIds(): AsyncIterable<string>; readonly events: JournalEventBus;}InMemoryJournal implements all of this with a Map<string, PersistentEvent[]>
internally. Operations are:
append— push events onto the stream’s array. Concurrency check onexpectedSeq(throwsJournalConcurrencyErrorif the current head doesn’t match).read— slice the array by sequence number range.highestSeq— read the last entry’s seqNr (or 0).deleteEventsUpTo— splice off the prefix.persistenceIds— iterate the Map’s keys.
Every append also publishes the event to the in-process
JournalEventBus — so push-based queries (see
Push-based query) work
in real time without polling.
When to use it
Section titled “When to use it”Three legitimate cases:
- Tests — fast, no IO, no cleanup needed between tests. Spin up a fresh system, do your assertions, tear down.
- Development — when you don’t want to bother with a SQLite file during local iteration.
- Throwaway demos — when the data isn’t supposed to survive.
For anything that needs to persist across restarts, switch to SQLite or Cassandra.
Test pattern
Section titled “Test pattern”import { describe, it, beforeEach, afterEach } from 'bun:test';import { TestKit, InMemoryJournal, InMemorySnapshotStore, PersistenceExtensionId } from 'actor-ts';
describe('OrderActor', () => { let tk: TestKit;
beforeEach(() => { tk = TestKit.create(); tk.system.extension(PersistenceExtensionId).configure({ journal: new InMemoryJournal(), snapshotStore: new InMemorySnapshotStore(), }); });
afterEach(async () => { await tk.shutdown(); });
it('replays events on restart', async () => { const probe = tk.createTestProbe();
// Create + send some commands let order = tk.system.actorOf(Props.create(() => new OrderActor('order-1'))); order.tell({ kind: 'place', sku: 'book-1' }); order.tell({ kind: 'place', sku: 'book-2' }); await probe.expectMsg({ kind: 'placed', sku: 'book-2' });
// Stop + re-spawn — same persistence ID await tk.system.stop(order); order = tk.system.actorOf(Props.create(() => new OrderActor('order-1'))); order.tell({ kind: 'view', replyTo: probe });
// Recovered state should reflect both placed events await probe.expectMsg({ items: ['book-1', 'book-2'] }); });});A fresh InMemoryJournal per test (via per-test TestKit) makes
each case isolated — no state leakage between tests.
What it doesn’t do
Section titled “What it doesn’t do”Switching to a real journal
Section titled “Switching to a real journal”// Before (test / dev):system.extension(PersistenceExtensionId).configure({ journal: new InMemoryJournal(),});
// After (production):import { SqliteJournal } from 'actor-ts';system.extension(PersistenceExtensionId).configure({ journal: new SqliteJournal({ path: '/var/lib/my-app/events.db' }),});No code changes inside actors — the Journal interface is the
same. Just swap the implementation.
This is why we say “use in-memory for tests and dev” — the production switch is one line, and the test suite continues to run against fast in-memory journals.
Where to next
Section titled “Where to next”- Persistence overview — the bigger picture.
- SQLite journal — the single-node production default.
- Cassandra journal — for multi-node production.
- PersistentActor — what’s writing the events.
- Snapshot stores — In-memory — the companion in-memory snapshot store.