Amazon DynamoDB
Overview
The cloudstub-dynamodb module mocks Amazon DynamoDB. Its operation set is generated from the AWS
DynamoDB Smithy model; AWS SDK v2 drives it with the JSON / X-Amz-Target protocol.
The table and item operations are state-backed: an item written by PutItem is returned by a
later GetItem, Query, or Scan, survives a restart when a persistent store directory is
configured, and is visible through the REST API. Items are stored keyed by a digest
of their primary-key attribute values, so a write to the same key overwrites the earlier item. The
remaining operations return well-formed but stateless placeholder responses.
See Supported operations for the full list.
Standalone usage
Start the standalone server with the DynamoDB module enabled (it is auto-downloaded if not already in the plugin directory):
Applications talk to it through the AWS SDK by pointing the client's endpoint at the mock port
(http://localhost:4566), see the Test example for a DynamoDbClient setup and
Standalone Mode for the full configuration.
Create a table, put an item, and read it back with the AWS CLI:
$ aws dynamodb create-table --endpoint-url http://localhost:4566 \
--table-name orders \
--key-schema AttributeName=id,KeyType=HASH \
--attribute-definitions AttributeName=id,AttributeType=S \
--billing-mode PAY_PER_REQUEST
$ aws dynamodb put-item --endpoint-url http://localhost:4566 \
--table-name orders \
--item '{"id":{"S":"o-1"},"status":{"S":"placed"}}'
$ aws dynamodb get-item --endpoint-url http://localhost:4566 \
--table-name orders --key '{"id":{"S":"o-1"}}'
{
"Item": {
"id": {"S": "o-1"},
"status": {"S": "placed"}
}
}
To inspect table state from the terminal, use the CLI (clb), or call the
REST API on the API port (4567) directly. Parameters are passed as query-string
values. List tables and scan the items an application wrote through the SDK:
The REST API and the SDK share the same state store, so an item your application puts through the SDK
is listed by GET /api/dynamodb/scan, and vice versa. The REST surface is read-oriented (items are
typed attribute maps that do not map cleanly to query-string parameters), so writes go through the
AWS SDK. See REST API access for the full route set.
Test example
In embedded mode, add cloudstub-dynamodb (see Getting Started) and exercise
the service end to end with CloudStubExtension:
import io.cloudstub.junit.CloudStubExtension;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import software.amazon.awssdk.auth.credentials.AnonymousCredentialsProvider;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
import software.amazon.awssdk.services.dynamodb.model.*;
import java.net.URI;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.*;
@ExtendWith(CloudStubExtension.class)
class OrderTableTest {
@Test
void itemPutIsReadBack() {
DynamoDbClient ddb = DynamoDbClient.builder()
.endpointOverride(URI.create(System.getProperty("aws.endpoint-url")))
.credentialsProvider(AnonymousCredentialsProvider.create())
.region(Region.US_EAST_1)
.build();
ddb.createTable(b -> b.tableName("orders")
.keySchema(KeySchemaElement.builder().attributeName("id").keyType(KeyType.HASH).build())
.attributeDefinitions(AttributeDefinition.builder()
.attributeName("id").attributeType(ScalarAttributeType.S).build())
.billingMode(BillingMode.PAY_PER_REQUEST));
ddb.putItem(b -> b.tableName("orders")
.item(Map.of("id", AttributeValue.fromS("o-1"), "status", AttributeValue.fromS("placed"))));
GetItemResponse response = ddb.getItem(b -> b.tableName("orders")
.key(Map.of("id", AttributeValue.fromS("o-1"))));
assertEquals("placed", response.item().get("status").s());
}
}
REST API access
The module exposes a REST API under /api/dynamodb/…. These routes read the same
state as the AWS-protocol stubs, so an item put with the SDK is listed by GET /api/dynamodb/scan.
Parameters are passed as query-string values (e.g. GET /api/dynamodb/scan?table=orders).
| Route | Parameters | Description |
|---|---|---|
GET /api/dynamodb/list-tables |
— | List tables |
GET /api/dynamodb/describe-table |
table |
Describe a table |
GET /api/dynamodb/scan |
table |
List all items in a table |
Supported operations
State-backed operations return live data from the shared state store:
| Operation | Behavior |
|---|---|
CreateTable |
Records the table and its key schema; returns the TableDescription |
DescribeTable |
Returns the table description, including the live ItemCount |
DeleteTable |
Removes the table and its items |
ListTables |
Returns the names of all created tables |
PutItem |
Stores an item keyed by its primary key; overwrites an existing item |
GetItem |
Returns the stored item for a key, or no item |
DeleteItem |
Removes an item by key |
UpdateItem |
Applies a SET/REMOVE update expression (or legacy AttributeUpdates) |
Query |
Returns items matching the partition key in KeyConditionExpression |
Scan |
Returns every item in the table |
BatchWriteItem |
Applies the put and delete requests across tables |
BatchGetItem |
Returns the stored items for the requested keys across tables |
The remaining operations are registered and return well-formed but stateless placeholder responses;
they do not read or mutate state: UpdateTable, DescribeLimits, DescribeTimeToLive,
UpdateTimeToLive, DescribeContinuousBackups, ListTagsOfResource, TagResource,
UntagResource.
Limitations
- Secondary indexes (GSI/LSI) are not maintained: a
QueryorScanwith anIndexNamereads the base table, matching on the base table's partition key only. Querymatches the partition key only. Sort-key range conditions (begins_with,BETWEEN,<,>) inKeyConditionExpressionare ignored and the whole partition is returned.FilterExpressiononQueryandScanis not applied.ProjectionExpression, pagination (Limit,ExclusiveStartKey,LastEvaluatedKey), andScanIndexForwardare not applied.- Conditional writes (
ConditionExpression,Expected) are not evaluated: writes always succeed. UpdateItemsupportsSETandREMOVEwithExpressionAttributeValues/ExpressionAttributeNames, and legacyAttributeUpdates(PUT/DELETE). Arithmetic (a = a + :n),list_append, and theADD/DELETEset actions are not applied.- Transactions (
TransactWriteItems,TransactGetItems), PartiQL (ExecuteStatement,BatchExecuteStatement,ExecuteTransaction), streams, global tables, backups, imports/exports, and time-to-live expiry are not simulated. - Provisioned-throughput and capacity accounting are not simulated:
ConsumedCapacityis not returned and requests are never throttled.
See also: Troubleshooting for common integration problems and workarounds.