コンテンツにスキップ
日本語

MongoDB

このコンテンツはまだ日本語訳がありません。

The MongoDB backend provides all four persistence components against a single MongoDB deployment, via the mongodb driver:

  • MongoJournal — the event journal for PersistentActors.
  • MongoSnapshotStore — snapshots to bound recovery.
  • MongoDurableStateStore — key-value durable state for DurableStateActors.
  • MongoQuery — an indexed currentEventsByTag, which the relational backends do not have.

It is the first document-store backend, and it needs no replica set: see No transactions required.

mongodb is an optional peer dependency. Pin it to version 6:

Terminal window
bun add mongodb@^6

The framework lazy-imports the driver only when a MongoDB store is first used, so it stays out of your bundle until you opt in.

Register the journal + snapshot store against the PersistenceExtension and receive a ready-to-use durable-state store. Set the connection once on the composite and all three components inherit it:

import {
ActorSystem,
ActorSystemOptions,
MongoDurableStateStoreOptions,
MongoJournalOptions,
MongoSnapshotStoreOptions,
PersistenceExtensionId,
RegisterMongoPluginsOptions,
registerMongoPlugins,
} from 'actor-ts';
const systemOptions = ActorSystemOptions.create()
// Select the MongoDB plugins as the active journal + snapshot store.
.withConfig({
'actor-ts': {
persistence: {
journal: { plugin: 'actor-ts.persistence.journal.mongodb' },
'snapshot-store': { plugin: 'actor-ts.persistence.snapshot-store.mongodb' },
},
},
});
const system = ActorSystem.create('my-app', systemOptions);
const ext = system.extension(PersistenceExtensionId);
const mongoSnapshotStoreOptions = MongoSnapshotStoreOptions.create()
.withKeepN(3);
const registerOptions = RegisterMongoPluginsOptions.create()
.withUrl('mongodb://user:pass@localhost:27017')
.withDatabaseName('app')
.withJournal(MongoJournalOptions.create() /* .withEventsCollection(...) */)
.withSnapshotStore(mongoSnapshotStoreOptions)
.withDurableStateStore(MongoDurableStateStoreOptions.create() /* .withCollection(...) */);
const { durableStateStore } = registerMongoPlugins(ext, registerOptions);

registerMongoPlugins registers the journal + snapshot store via the extension (selected by the config plugin IDs above) and returns the durable-state store. PersistenceExtension has no durable-state registry — hand durableStateStore to your DurableStateActor settings directly.

A MongoClient is a connection pool in its own right, so passing one is the efficient shape when all components target the same deployment:

import { MongoClient } from 'mongodb';
const client = new MongoClient('mongodb://localhost:27017');
await client.connect();
const registerMongoPluginsOptions = RegisterMongoPluginsOptions.create()
.withClient(client)
.withDatabaseName('app');
registerMongoPlugins(ext, registerMongoPluginsOptions);

A shared client is caller-owned: no store closes it, so close it yourself during shutdown. Pass url instead and each store builds its own client and closes it on close().

  • MongoDB is already your database. The largest driver in the ecosystem, and the one many teams already operate.
  • You want indexed tag queries. Only MongoDB, SQLite/libSQL and Cassandra push currentEventsByTag down to storage; the relational backends poll.
  • You want a single deployment for events and read models. Projections can write their views into the same database.
type MongoConnection = {
url?: string; // mongodb:// or mongodb+srv://
databaseName?: string; // default 'actor_ts'
clientOptions?: Record<string, unknown>; // { tls, authSource, maxPoolSize, … }
client?: MongoClientLike; // pre-built / shared client
};
interface MongoJournalOptions extends MongoConnection {
eventsCollection?: string; // default 'events'; its mark collection is '<it>_meta'
autoCreateIndexes?: boolean; // default true
}
interface MongoSnapshotStoreOptions extends MongoConnection {
snapshotsCollection?: string; // default 'snapshots'
keepN?: number; // keep newest N per pid; default 3, <=0 keeps all
autoCreateIndexes?: boolean;
}
interface MongoDurableStateStoreOptions extends MongoConnection {
collection?: string; // default 'durable_state'
}

Options are validated when the store is constructed, so a bad URL scheme or a database name MongoDB would refuse (one containing /, \, ., " or a space) fails at wiring time rather than on the first write.

CollectionShapeIndex
events{ persistenceId, sequenceNr, payload, tags?, timestamp }unique (persistenceId, sequenceNr); (tags, timestamp)
events_meta{ _id: persistenceId, deletedTo }_id
snapshots{ persistenceId, sequenceNr, payload, timestamp }unique (persistenceId, sequenceNr)
durable_state{ _id: persistenceId, revision, payload, timestamp }_id
  • Journal append reads the current head and then inserts. The unique compound index rejects a racing writer with server error 11000, which is translated into JournalConcurrencyError — the same two-layer scheme as the SQL backends (a head check for the ordinary case, a conditional write for the race), with error 11000 in the role of SQLSTATE 23505.
  • Durable-state CAS puts the revision in the filter: updateOne({ _id, revision: expected }, …). A mismatch matches nothing, so matchedCount === 0 means the stored revision diverged. A create is insertOne, where _id rejects a duplicate.
  • The compaction mark is monotonic via $max, which writes only when the new value is greater — exactly what GREATEST does in the SQL dialects.

MongoDB multi-document transactions need a replica set, and this backend deliberately does not use them, so it works on a standalone mongod.

That is sound rather than lucky. Appends are contiguous from the head, so two writers that agree on the head both try the same first sequence number: the loser’s insertMany fails on its very first document, and ordered: true stops the batch there, so it writes nothing. A partial append is not reachable through contention — which is the only thing a transaction would have bought.

MongoQuery gives currentEventsByTag an indexed path. Because tags is an array, MongoDB indexes it as a multikey index — one entry per tag per event — so a query on one tag plus a timestamp lower bound walks a contiguous range instead of scanning:

import { MongoQuery, offsetStart } from 'actor-ts';
const query = new MongoQuery(journal);
const tagged = await query.currentEventsByTag({ all: ['ledger'], not: ['audit'] }, offsetStart);

The server pre-filters on one tag; all past the first tag, cross-tag any and not are refined in memory, exactly as in the SQLite and Cassandra paths. A filter with only not has nothing to pre-filter on and falls back to the journal-walking scan.

  • PostgreSQL — the relational counterpart, with transactional multi-event appends.
  • Cassandra journal — the other distributed store with an indexed tag path.
  • Durable state — the state-oriented alternative to event sourcing.
  • Snapshots — bound the recovery scan.