Skip to main content

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

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.

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_")));

// 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:

MethodDescription
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.

Domain Methods

MethodDescriptionAvailable On
TableName(string)DynamoDB table nameAll 6 builders
TablePrefix(string)Prefix for table names5 builders (not CDC)
StreamArn(string)DynamoDB Streams ARNCDC only
ProcessorName(string)CDC processor nameCDC only
WithStateStore(...)CDC state store configurationCDC 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:

  • UseTransactionalWrite = true (default) with > 100 events → the append is rejected up front; split the batch into appends of at most 100 events.
  • UseTransactionalWrite = false → the consumer has opted into the non-atomic per-item PutItem path, so batches larger than 100 events are permitted as documented non-atomic behavior.

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