Amazon DynamoDB Provider
The DynamoDB provider implements ICloudNativePersistenceProvider for AWS serverless workloads with hash/sort key support, DynamoDB Streams integration, and transactional batch operations.
Before You Start
- .NET 10.0
- An AWS account with DynamoDB access
- Familiarity with data access and IDb interface
Installation
dotnet add package Excalibur.Data.DynamoDb
Dependencies: Excalibur.Data.Abstractions, AWSSDK.DynamoDBv2
Quick Start
using Microsoft.Extensions.DependencyInjection;
services.AddExcaliburDynamoDb(dynamo =>
{
dynamo.Region(RegionEndpoint.USEast1)
.TablePrefix("MyApp_");
});
Registration
The builder API (IDynamoDBDataBuilder) supports 4 canonical connection overloads plus configuration binding:
// AWS Region (uses default credentials / IAM role)
services.AddExcaliburDynamoDb(dynamo =>
dynamo.Region(RegionEndpoint.USEast1).TableName("MyTable"));
// Local DynamoDB / LocalStack
services.AddExcaliburDynamoDb(dynamo =>
dynamo.ServiceUrl("http://localhost:8000").TableName("MyTable"));
// Existing IAmazonDynamoDB client
services.AddExcaliburDynamoDb(dynamo =>
dynamo.Client(existingClient).TableName("MyTable"));
// Client factory (for custom configuration)
services.AddExcaliburDynamoDb(dynamo =>
dynamo.ClientFactory(sp => new AmazonDynamoDBClient(credentials, region))
.TableName("MyTable"));
// IConfiguration binding
services.AddExcaliburDynamoDb(dynamo =>
dynamo.BindConfiguration("DynamoDb"));
All registrations include ValidateOnStart for options validation.
The event store writes each event's declared [MessageName] -- not its CLR type name -- to the stored event-type field, and resolves that name back to a CLR type through the registered event-type registry on read. See Stable Message Names.
Projection Store
Projections are stored flat at the document root. Framework metadata is isolated under a _projection nested object to avoid collisions with your projection properties. See Projections — Document Storage Format for details.
Subsystem Registration
Each Excalibur subsystem supports DynamoDB via its own builder:
services.AddExcalibur(excalibur =>
{
// Event Sourcing
excalibur.AddEventSourcing(es =>
es.UseDynamoDb(dynamo =>
dynamo.Region(RegionEndpoint.USEast1)
.TableName("Events")
.TablePrefix("MyApp_")));
// Configured by region, so a matching Streams client is built for you and the
// change feed works with no further configuration. See "Streams Methods" below
// for when you supply your own client.
// Saga
excalibur.AddSaga(saga =>
saga.UseDynamoDb(dynamo =>
dynamo.Region(RegionEndpoint.USEast1)
.TableName("Sagas")));
// Inbox (idempotency)
excalibur.AddInbox(inbox =>
inbox.UseDynamoDb(dynamo =>
dynamo.Region(RegionEndpoint.USEast1)
.TableName("Inbox")));
// Outbox (reliable messaging)
excalibur.AddOutbox(outbox =>
outbox.UseDynamoDb(dynamo =>
dynamo.Region(RegionEndpoint.USEast1)
.TableName("Outbox")));
});
Change Data Capture
services.AddCdcProcessor(cdc =>
{
cdc.UseDynamoDb(dynamo =>
{
dynamo.TableName("Orders")
.StreamArn("arn:aws:dynamodb:us-east-1:123456789:table/Orders/stream/2026-01-01T00:00:00.000")
.ProcessorName("order-processor")
.WithStateStore(
sp => new AmazonDynamoDBClient(),
state => state.TableName("CdcState"));
})
.TrackTable("Orders", t => t.MapAll<OrderChangedEvent>())
.EnableBackgroundProcessing();
});
Builder Methods
Connection Methods
All 5 non-CDC subsystem builders share the same connection methods:
| Method | Description |
|---|---|
ServiceUrl(string) | Local DynamoDB or LocalStack endpoint URL |
Region(RegionEndpoint) | AWS region (uses default credential chain) |
Client(IAmazonDynamoDB) | Pre-configured client instance |
ClientFactory(Func<IServiceProvider, IAmazonDynamoDB>) | Factory for custom client creation |
BindConfiguration(string) | Bind from IConfiguration section |
Connection methods follow last-wins semantics — calling Client() after Region() replaces the region-based connection.
Streams Methods
Available on the event sourcing builder (IDynamoDBEventSourcingBuilder) only.
| Method | Description |
|---|---|
StreamsClient(IAmazonDynamoDBStreams) | Pre-configured Streams client instance, enabling the change feed |
StreamsClientFactory(Func<IServiceProvider, IAmazonDynamoDBStreams>) | Factory resolving the Streams client from DI, enabling the change feed |
You do not need either of these unless you consume a change feed. The event store appends, loads, and reads versions through the DynamoDB client alone; a store built without a Streams client is fully functional for those operations and simply reports the change feed as unavailable.
When you do consume one, whether you must supply a client depends on how the connection was configured:
| Connection configured by | Streams client |
|---|---|
ServiceUrl(...) or Region(...) | Built for you automatically — nothing to configure |
Client(...) or ClientFactory(...) | You must supply one, or the change feed is unavailable |
The registration will not guess at the endpoint and credentials behind a client you supplied, so it builds no Streams client on those paths. Supplying your own also means the change feed runs under your own credentials, endpoint, and telemetry:
using Amazon.DynamoDBStreams;
using Amazon.DynamoDBv2;
services.AddExcalibur(excalibur => excalibur.AddEventSourcing(es =>
es.UseDynamoDb(dynamo =>
dynamo.Client(myDynamoDbClient)
.StreamsClient(myStreamsClient)
.TableName("Events"))));
Unlike the connection methods, these are orthogonal to the connection mode: selecting or changing a
connection method neither sets nor clears the Streams client. StreamsClient and StreamsClientFactory
are last-wins against each other.
See Event Sourcing — Change Feed for the full walkthrough.
Domain Methods
| Method | Description | Available On |
|---|---|---|
TableName(string) | DynamoDB table name | All 6 builders |
TablePrefix(string) | Prefix for table names | 5 builders (not CDC) |
StreamArn(string) | DynamoDB Streams ARN | CDC only |
StreamsClient(...) / StreamsClientFactory(...) | Streams client for the change feed | Event sourcing only |
ProcessorName(string) | CDC processor name | CDC only |
WithStateStore(...) | CDC state store configuration | CDC only |
Partition and Sort Keys
DynamoDB uses hash keys and optional sort keys:
var key = new PartitionKey("USER#user-123", "/pk");
var result = await provider.GetByIdAsync<UserProfile>(
"PROFILE#main", key,
consistencyOptions: null,
cancellationToken: ct);
Batch Operations
Execute multiple operations atomically:
var batchResult = await provider.ExecuteBatchAsync(
partitionKey,
operations,
cancellationToken: ct);
Atomic event-store append limit
The DynamoDB event store appends a batch as a single all-or-nothing TransactWriteItems call. DynamoDB hard-caps TransactWriteItems at 100 items, and offers no atomic primitive beyond that. To guarantee the append contract (no torn event-stream prefix), the store rejects an atomic append of more than 100 events at the boundary, before any write:
- More than 100 events → the append is rejected up front; split the batch into appends of at most 100 events. The exception carries the count you supplied and the limit, so the split is mechanical.
- 2 to 100 events → one
TransactWriteItemscall, all-or-nothing. - A single event → one conditional
PutItem, which is atomic on its own. It does not use a transaction, so it does not incur the doubled write cost or the additional IAM permissionTransactWriteItemsrequires.
There is no opt-out that accepts a larger append. An earlier release offered one, and it bought throughput by writing items individually — which leaves a torn prefix if it fails partway, and no later read can tell that apart from a stream that was simply never written further.
Configuration Binding
Bind DynamoDB options from appsettings.json:
{
"DynamoDb": {
"Connection": {
"Region": "us-east-1",
"ServiceUrl": null
},
"DefaultTableName": "MyTable",
"DefaultPartitionKeyAttribute": "pk",
"DefaultSortKeyAttribute": "sk",
"UseConsistentReads": false
}
}
services.AddExcaliburDynamoDb(dynamo =>
dynamo.BindConfiguration("DynamoDb"));
See Also
- Data Providers Overview — Architecture and core abstractions
- Cosmos DB Provider — Azure cloud-native alternative
- Firestore Provider — Google Cloud alternative