Docs / DynamoDB

DynamoDB

LocalEmu's DynamoDB runs on pure-Python Moto. No Java, no JVM, no external binary. A LocalEmu layer wraps Moto to add full DynamoDB Streams (INSERT / MODIFY / REMOVE records across all four view types), transaction pre and post-processing so streams capture the right NewImage and OldImage, Global Tables v1 and v2 with replica routing, TTL bookkeeping, error injection, and state persistence. 100% of the 57 documented DynamoDB operations are implemented (34 with custom LocalEmu code, 23 forwarded to Moto, 0 missing). DynamoDB Streams: 4 of 4 operations, all custom.

Operation-level breakdown: see the DynamoDB coverage matrix and the DynamoDB Streams coverage matrix.

Quick start

Terminal
$ awsemu dynamodb create-table \
    --table-name users \
    --attribute-definitions AttributeName=id,AttributeType=S \
    --key-schema AttributeName=id,KeyType=HASH \
    --billing-mode PAY_PER_REQUEST
TableStatus: ACTIVE

$ awsemu dynamodb put-item \
    --table-name users \
    --item '{"id":{"S":"u1"},"name":{"S":"Alice"},"age":{"N":"30"}}'

$ awsemu dynamodb get-item \
    --table-name users \
    --key '{"id":{"S":"u1"}}'
{
  "Item": {
    "id":   {"S": "u1"},
    "name": {"S": "Alice"},
    "age":  {"N": "30"}
  }
}

$ awsemu dynamodb update-item \
    --table-name users \
    --key '{"id":{"S":"u1"}}' \
    --update-expression 'SET age = :a, #s = :st' \
    --expression-attribute-names '{"#s":"status"}' \
    --expression-attribute-values '{":a":{"N":"31"},":st":{"S":"premium"}}'

The AWS CLI, boto3, the JavaScript and Go SDKs, Terraform, the AWS CDK, and Pulumi all connect with only the endpoint URL changed.

Architecture

The DynamoDB provider lives at services/dynamodb/provider.py. Requests follow this path:

State is held in services/dynamodb/models.py (table definitions, TTL specifications, backups, streaming destinations, global table replicas, tags). When PERSISTENCE=1 is set, the store is serialised and restored across LocalEmu restarts.

DynamoDB Streams (services/dynamodbstreams/) is 100% custom. Under the hood, stream records are stored on a Kinesis stream named __ddb_stream_<TableName>; the DynamoDB Streams API surface (DescribeStream, GetShardIterator, GetRecords, ListStreams) translates Kinesis shard IDs into DynamoDB shard IDs and shapes records as DynamoDB stream events. Callers see DynamoDB Streams; the Kinesis backing is an implementation detail.

Configuration

Four DynamoDB-specific environment variables. All others (region, account, gateway port) come from the global LocalEmu configuration.

VariableDefaultPurpose
DYNAMODB_ERROR_PROBABILITY0.0Inject random faults into all DynamoDB operations. Useful for chaos testing your retry logic.
DYNAMODB_READ_ERROR_PROBABILITY0.0Inject faults only on read operations: GetItem, Query, Scan, BatchGetItem, TransactGetItems.
DYNAMODB_WRITE_ERROR_PROBABILITY0.0Inject faults only on write operations: PutItem, UpdateItem, DeleteItem, BatchWriteItem, TransactWriteItems.
DYNAMODB_REMOVE_EXPIRED_ITEMSfalseWhen true, a background worker sweeps items whose TTL attribute has elapsed. Off by default: items past their TTL stay readable until you enable the sweeper.

Features supported

FeatureNotes
Table CRUDCreateTable, UpdateTable, DeleteTable, DescribeTable, ListTables.
Item CRUDPutItem, GetItem, UpdateItem, DeleteItem with stream emission on each.
Batch operationsBatchWriteItem, BatchGetItem with the AWS-standard 25 and 100 item limits and proper unprocessed-items semantics.
Query and ScanKey-condition expressions, filter expressions (LocalEmu rejects filter references to primary-key attributes the way real AWS does), projections, pagination.
Conditional expressionsattribute_exists, attribute_not_exists, attribute_type, begins_with, contains, size, comparison operators, boolean composition. Failed conditions raise ConditionalCheckFailedException.
Update expressionsSET, ADD, REMOVE, DELETE with ExpressionAttributeNames and ExpressionAttributeValues.
Global Secondary IndexesCreated and updated via CreateTable / UpdateTable, queryable with the index-name parameter on Query.
Local Secondary IndexesCreated at table creation time, queryable via Query with the index-name parameter.
StreamsAll four view types: KEYS_ONLY, NEW_IMAGE, OLD_IMAGE, NEW_AND_OLD_IMAGES. See the next section.
TransactionsTransactWriteItems (Put, Update, Delete, ConditionCheck) and TransactGetItems. Failures roll the entire transaction back and raise TransactionCanceledException.
TagsTagResource, UntagResource, ListTagsOfResource. Stored cross-region in the LocalEmu store.
Time-To-LiveTTL specification CRUD. Real background expiry is opt-in via DYNAMODB_REMOVE_EXPIRED_ITEMS=true.
BackupsCreateBackup, RestoreTableFromBackup, ListBackups, DescribeBackup, DeleteBackup.
Point-in-Time RecoveryDescribeContinuousBackups, UpdateContinuousBackups.
Global Tables (v1 + v2)Replica metadata stored, requests routed to the primary region. Single backing copy.
Import / ExportImportTable (S3 source), ExportTableToPointInTime (S3 destination).
Billing modesBoth PROVISIONED and PAY_PER_REQUEST. Swap with UpdateTable --billing-mode.
PartiQLExecuteStatement, BatchExecuteStatement, ExecuteTransaction.

DynamoDB Streams

Streams emit on every successful write, with the record content shaped by the stream's view type:

StreamViewTypeRecord contents
KEYS_ONLYOnly the primary key attributes.
NEW_IMAGEKeys plus the item as it looks after the write.
OLD_IMAGEKeys plus the item as it looked before the write.
NEW_AND_OLD_IMAGESKeys plus both the before and after images.

Each record carries an eventName of INSERT, MODIFY, or REMOVE, a monotonic SequenceNumber, and an ApproximateCreationDateTime. Shard iterators support TRIM_HORIZON, LATEST, AT_SEQUENCE_NUMBER, and AFTER_SEQUENCE_NUMBER.

Terminal
$ awsemu dynamodb create-table \
    --table-name events \
    --attribute-definitions AttributeName=id,AttributeType=S \
    --key-schema AttributeName=id,KeyType=HASH \
    --billing-mode PAY_PER_REQUEST \
    --stream-specification StreamEnabled=true,StreamViewType=NEW_AND_OLD_IMAGES

$ STREAM_ARN=$(awsemu dynamodb describe-table --table-name events \
    --query 'Table.LatestStreamArn' --output text)

$ awsemu dynamodb put-item --table-name events --item '{"id":{"S":"e1"},"v":{"N":"1"}}'
$ awsemu dynamodb update-item --table-name events --key '{"id":{"S":"e1"}}' \
    --update-expression 'SET v = :v' --expression-attribute-values '{":v":{"N":"2"}}'
$ awsemu dynamodb delete-item --table-name events --key '{"id":{"S":"e1"}}'

# Three stream records emitted: INSERT, MODIFY, REMOVE
$ SHARD_ID=$(awsemu dynamodbstreams describe-stream --stream-arn $STREAM_ARN \
    --query 'StreamDescription.Shards[0].ShardId' --output text)
$ ITER=$(awsemu dynamodbstreams get-shard-iterator \
    --stream-arn $STREAM_ARN --shard-id $SHARD_ID \
    --shard-iterator-type TRIM_HORIZON \
    --query 'ShardIterator' --output text)
$ awsemu dynamodbstreams get-records --shard-iterator $ITER \
    --query 'Records[].dynamodb.{event:Keys, view:StreamViewType}'

Wiring a Lambda function to the stream uses the standard event-source-mapping API. LocalEmu's Lambda poller (services/lambda_/event_source_mapping/pollers/dynamodb_poller.py) reads records from the stream and invokes the function once per batch.

Terminal
# Wire a Lambda function to the DynamoDB stream
$ awsemu lambda create-event-source-mapping \
    --function-name on-event \
    --event-source-arn $STREAM_ARN \
    --starting-position TRIM_HORIZON \
    --batch-size 10

UUID: 8a7b6c5d-...
State: Enabled

# Now PutItem / UpdateItem / DeleteItem on the table will invoke the Lambda
# with the stream batch as its event payload.

Examples

Global Secondary Index with Query

Terminal
$ awsemu dynamodb create-table \
    --table-name orders \
    --attribute-definitions \
        AttributeName=order_id,AttributeType=S \
        AttributeName=customer_id,AttributeType=S \
    --key-schema AttributeName=order_id,KeyType=HASH \
    --billing-mode PAY_PER_REQUEST \
    --global-secondary-indexes \
        'IndexName=by-customer,KeySchema=[{AttributeName=customer_id,KeyType=HASH}],Projection={ProjectionType=ALL}'

# Query the GSI
$ awsemu dynamodb query \
    --table-name orders \
    --index-name by-customer \
    --key-condition-expression 'customer_id = :c' \
    --expression-attribute-values '{":c":{"S":"c-42"}}'

TransactWriteItems with rollback on ConditionCheck

Terminal
$ awsemu dynamodb transact-write-items --transact-items '[
  {"Put":    {"TableName":"orders","Item":{"order_id":{"S":"o1"},"total":{"N":"99"}}}},
  {"Update": {"TableName":"users","Key":{"id":{"S":"u1"}},
              "UpdateExpression":"ADD spend :v",
              "ExpressionAttributeValues":{":v":{"N":"99"}}}},
  {"ConditionCheck": {"TableName":"users","Key":{"id":{"S":"u1"}},
                      "ConditionExpression":"attribute_exists(id)"}}
]'

# If any leg fails (e.g. ConditionCheck returns false), the entire
# transaction rolls back and no items are written.

Limits and defaults

The AWS-standard DynamoDB limits (400 KB max item size, 25 items per BatchWriteItem, 100 items per BatchGetItem, 20 GSI per table, 5 LSI per table, 25 items per transaction) are enforced by the Moto backend. LocalEmu adds the following:

LimitValueSource
Internal Scan / Query paginator size100services/dynamodb/provider.py:202
DynamoDB Streams shard iterator cache10,000 entriesservices/dynamodbstreams/provider.py:59
ConsumedCapacity per response5 units (hardcoded)services/dynamodb/provider.py:2127

Known limitations

All 57 DynamoDB operations and all 4 DynamoDB Streams operations are implemented. Behavioural caveats:

The data plane semantics are Moto's: condition evaluation, update expressions, type coercion, key constraints, and batch unprocessed-items behaviour all follow Moto's implementation rather than re-implementing AWS DynamoDB. Where Moto and AWS diverge (extreme number-precision rounding, certain reserved-attribute-name error codes), LocalEmu inherits the divergence.