DynamoDB
이 콘텐츠는 아직 번역되지 않았습니다.
The DynamoDB backend provides all three persistence components against Amazon
DynamoDB, via @aws-sdk/client-dynamodb:
DynamoDbJournal— the event journal forPersistentActors.DynamoDbSnapshotStore— snapshots to bound recovery.DynamoDbDurableStateStore— key-value durable state forDurableStateActors.
It has the strongest optimistic concurrency of any backend the framework ships — see Concurrency model — and it is fully serverless: no instance to run, on-demand capacity by default.
Install
Section titled “Install”@aws-sdk/client-dynamodb is an optional peer dependency:
bun add @aws-sdk/client-dynamodbIt is the same SDK family already used for the S3 object-storage backend, so if you use that, the runtime story is one you already have. The framework lazy-imports it only when a DynamoDB store is first used.
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, DynamoDbDurableStateStoreOptions, DynamoDbJournalOptions, DynamoDbSnapshotStoreOptions, PersistenceExtensionId, RegisterDynamoDbPluginsOptions, registerDynamoDbPlugins,} from 'actor-ts';
const systemOptions = ActorSystemOptions.create() // Select the DynamoDB plugins as the active journal + snapshot store. .withConfig({ 'actor-ts': { persistence: { journal: { plugin: 'actor-ts.persistence.journal.dynamodb' }, 'snapshot-store': { plugin: 'actor-ts.persistence.snapshot-store.dynamodb' }, }, }, });const system = ActorSystem.create('my-app', systemOptions);
const ext = system.extension(PersistenceExtensionId);const dynamoDbSnapshotStoreOptions = DynamoDbSnapshotStoreOptions.create() .withKeepN(3);const registerOptions = RegisterDynamoDbPluginsOptions.create() .withRegion('eu-central-1') .withJournal(DynamoDbJournalOptions.create() /* .withEventsTable(...) */) .withSnapshotStore(dynamoDbSnapshotStoreOptions) .withDurableStateStore(DynamoDbDurableStateStoreOptions.create() /* .withTable(...) */);const { durableStateStore } = registerDynamoDbPlugins(ext, registerOptions);Credentials come from the standard AWS provider chain (environment, shared
config, IAM role), so nothing about them is actor-ts-specific. Pass
withClientConfig({ credentials }) to override.
Local development
Section titled “Local development”dynamodb-local needs an endpoint override, and the SDK will not sign a request
without credentials even though the emulator ignores them:
const dynamoDbJournalOptions = DynamoDbJournalOptions.create() .withRegion('eu-central-1') .withEndpoint('http://localhost:8000') .withClientConfig({ credentials: { accessKeyId: 'local', secretAccessKey: 'local' } });When to use it
Section titled “When to use it”- You are on AWS and want no database to operate. No instance, no connection pool, no patching.
- You want the strongest append guarantee available. The transactional append is atomic, which not even the relational backends manage for a multi-event batch under contention.
- Your write pattern is spiky. On-demand billing needs no capacity planning.
Configuration
Section titled “Configuration”type DynamoDbConnection = { region?: string; // eu-central-1, … endpoint?: string; // http(s) URL — dynamodb-local, LocalStack clientConfig?: Record<string, unknown>; // { credentials, maxAttempts, … } operations?: DynamoDbOperations; // pre-built / shared façade};
type DynamoDbTableProvisioning = { autoCreateTables?: boolean; // default true billingMode?: 'PAY_PER_REQUEST' | 'PROVISIONED'; // default on-demand provisionedThroughput?: { readCapacityUnits: number; writeCapacityUnits: number }; tableReadyTimeoutMs?: number; // default 30_000};
interface DynamoDbJournalOptions extends DynamoDbConnection, DynamoDbTableProvisioning { eventsTable?: string; // default 'actor_ts_events'}interface DynamoDbSnapshotStoreOptions extends DynamoDbConnection, DynamoDbTableProvisioning { snapshotsTable?: string; // default 'actor_ts_snapshots' keepN?: number; // keep newest N per pid; default 3, <=0 keeps all}interface DynamoDbDurableStateStoreOptions extends DynamoDbConnection, DynamoDbTableProvisioning { table?: string; // default 'actor_ts_durable_state'}Options are validated when the store is constructed. Two rules earn their
keep: a table name is checked against DynamoDB’s own charset and length, and
capacity units are rejected unless billingMode is PROVISIONED — AWS silently
ignores them otherwise, which makes the mistake invisible.
Tables
Section titled “Tables”| Table | Partition key | Sort key | Holds |
|---|---|---|---|
actor_ts_events | pid | seq (N) | one item per event; seq = 0 is the compaction mark |
actor_ts_snapshots | pid | seq (N) | one item per snapshot |
actor_ts_durable_state | pid | — | one item per persistence id |
With autoCreateTables (the default) each store creates its table on first use
and waits for it to become ACTIVE — CreateTable returns while the table
is still CREATING, and every operation against it fails until it flips. The
wait happens inside the lazy init, so the first caller blocks and everyone behind
it gets a table that works.
Concurrency model
Section titled “Concurrency model”- Journal append is one atomic transaction. Every event goes into a single
TransactWriteItems, eachPutcarryingConditionExpression: attribute_not_exists(pid)— “only if this(pid, seq)item does not exist”. A racing writer therefore cannot win a partial append: the transaction is all-or-nothing, and its cancellation becomesJournalConcurrencyError. The head read that precedes it is only an optimization, turning the common stale append into one cheap query instead of a rejected transaction. - Durable-state CAS is a native conditional write —
PutItem … attribute_not_exists(pid)to create,UpdateItem … revision = :expectedto update — needing neither a transaction nor a read-back. - The compaction mark only rises. It is updated with
attribute_not_exists(deletedTo) OR deletedTo < :value, which isGREATESTexpressed as a condition; a lower value is rejected, and that rejection is the expected outcome rather than an error.
Pitfalls
Section titled “Pitfalls”Where to next
Section titled “Where to next”- MongoDB — the other non-SQL backend, with an indexed tag query.
- PostgreSQL — if you would rather run a relational database.
- Durable state — the contract DynamoDB fits best.
- Snapshots — bound the recovery scan.
