Building with DynamoDB: A 4-Minute AWS CLI Tutorial
Learn to create a DynamoDB table, index it, and avoid common design traps — all from your terminal. This tutorial will help you understand the 10 GB item collection size limit (per partition key) that can trigger the ItemCollectionSizeLimitExceededException
if your design isn’t balanced.
Step 1: Create a Table
In this step, you’ll create a DynamoDB table with a partition key (CustomerID
) and a sort key (OrderID
). These two attributes define how your data will be uniquely stored and accessed.
aws dynamodb create-table \
--table-name OrdersTable \
--attribute-definitions \
AttributeName=CustomerID,AttributeType=S \
AttributeName=OrderID,AttributeType=S \
--key-schema \
AttributeName=CustomerID,KeyType=HASH \
AttributeName=OrderID,KeyType=RANGE \
--billing-mode PAY_PER_REQUEST
Output:
{
"TableDescription": {
"TableName": "OrdersTable",
"TableStatus": "CREATING",
"BillingModeSummary": { "BillingMode": "PAY_PER_REQUEST" },
"ItemCount": 0,
"TableSizeBytes": 0
}
}
You should see confirmation that the table is being created. The status CREATING
means AWS is provisioning it.
Check the table status until it becomes ACTIVE
:
aws dynamodb describe-table --table-name OrdersTable
Output:
{
"Table": {
"AttributeDefinitions": [
{
"AttributeName": "CustomerID",
"AttributeType": "S"
},
{
"AttributeName": "OrderID",
"AttributeType": "S"
}
],
"TableName": "OrdersTable",
"KeySchema": [
{
"AttributeName": "CustomerID",
"KeyType": "HASH"
},
{
"AttributeName": "OrderID",
"KeyType": "RANGE"
}
],
"TableStatus": "ACTIVE",
"CreationDateTime": "2025-10-16T18:23:47.104000-05:00",
"ProvisionedThroughput": {
"NumberOfDecreasesToday": 0,
"ReadCapacityUnits": 0,
"WriteCapacityUnits": 0
},
"TableSizeBytes": 0,
"ItemCount": 0,
"BillingModeSummary": {
"BillingMode": "PAY_PER_REQUEST"
}
}
}
"TableStatus": "ACTIVE"
confirms your table is ready for reads and writes.
Step 2: Insert Sample Data
Now, you’ll add some data to your table. Each item will represent an order for a specific customer.
aws dynamodb put-item \
--table-name OrdersTable \
--item '{"CustomerID": {"S": "CUST#1001"}, "OrderID": {"S": "ORD#5001"}, "Amount": {"N": "29.99"}, "Status": {"S": "SHIPPED"}}'
Output:
{}
An empty {}
means the item was inserted successfully.
Insert another record.
aws dynamodb put-item \
--table-name OrdersTable \
--item '{"CustomerID": {"S": "CUST#1001"}, "OrderID": {"S": "ORD#5002"}, "Amount": {"N": "45.00"}, "Status": {"S": "PENDING"}}'
Output:
{}
Each successful put-item
command returns an empty JSON response.
Step 3: Query by Partition Key
Here, you’ll retrieve all orders for a single customer.
aws dynamodb query \
--table-name OrdersTable \
--key-condition-expression "CustomerID = :cust" \
--expression-attribute-values '{":cust":{"S":"CUST#1001"}}'
Output:
{
"Items": [
{
"CustomerID": {"S": "CUST#1001"},
"OrderID": {"S": "ORD#5001"},
"Amount": {"N": "29.99"},
"Status": {"S": "SHIPPED"}
},
{
"CustomerID": {"S": "CUST#1001"},
"OrderID": {"S": "ORD#5002"},
"Amount": {"N": "45.00"},
"Status": {"S": "PENDING"}
}
],
"Count": 2,
"ScannedCount": 2
}
This shows both items you inserted earlier. Each object under Items
represents a record in DynamoDB.
Step 4: Add a Global Secondary Index (GSI)
Now, you’ll create an index on the Status attribute so you can query orders by their shipment status. Since Status is a low-cardinality key, be aware that this design pattern can lead to hot partitions and the 10 GB item-collection limit in large-scale applications.
aws dynamodb update-table \
--table-name OrdersTable \
--attribute-definitions AttributeName=Status,AttributeType=S \
--global-secondary-index-updates \
"[{\"Create\":{\"IndexName\":\"StatusIndex\", \
\"KeySchema\":[{\"AttributeName\":\"Status\",\"KeyType\":\"HASH\"}], \
\"Projection\":{\"ProjectionType\":\"ALL\"}}}]"
Output:
{
"TableDescription": {
"TableName": "OrdersTable",
"GlobalSecondaryIndexes": [
{
"IndexName": "StatusIndex",
"IndexStatus": "CREATING"
}
]
}
}
You should see the index in the CREATING
state. DynamoDB is now building it in the background.
Check the index status until it’s active. Run the command below every few seconds until it shows ACTIVE
.
aws dynamodb describe-table --table-name OrdersTable
Output:
{
"Table": {
"TableName": "OrdersTable",
"TableStatus": "ACTIVE",
"GlobalSecondaryIndexes": [
{
"IndexName": "StatusIndex",
"KeySchema": [
{
"AttributeName": "Status",
"KeyType": "HASH"
}
],
"Projection": {
"ProjectionType": "ALL"
},
"IndexStatus": "ACTIVE",
"ProvisionedThroughput": {
"NumberOfDecreasesToday": 0,
"ReadCapacityUnits": 0,
"WriteCapacityUnits": 0
},
"IndexSizeBytes": 0,
"ItemCount": 2
}
],
"BillingModeSummary": {
"BillingMode": "PAY_PER_REQUEST"
}
}
}
Here you’ll see both "TableStatus": "ACTIVE"
and "IndexStatus": "ACTIVE"
, confirming that the GSI is fully built and ready to query.
Step 5: Query by Status Using the GSI
Now that the index is ready, you can query all shipped orders by status.
aws dynamodb query \
--table-name OrdersTable \
--index-name StatusIndex \
--key-condition-expression "Status = :s" \
--expression-attribute-values '{":s":{"S":"SHIPPED"}}'
Output:
{
"Items": [
{
"CustomerID": {"S": "CUST#1001"},
"OrderID": {"S": "ORD#5001"},
"Amount": {"N": "29.99"},
"Status": {"S": "SHIPPED"}
}
],
"Count": 1,
"ScannedCount": 1
}
You should see only the items that match your query condition (Status = SHIPPED
).
Step 6: Clean Up
Finally, you’ll delete your table to avoid unnecessary charges.
aws dynamodb delete-table --table-name OrdersTable
Output:
{
"TableDescription": {
"TableName": "OrdersTable",
"TableStatus": "DELETING"
}
}
DELETING
confirms the removal process has started.
Verify that the table no longer exists.
aws dynamodb list-tables
Output:
{
"TableNames": []
}
An empty TableNames
list means your table was successfully deleted.
Wrap-Up
You’ve just built a working DynamoDB table, queried it by partition key and GSI, and observed how indexes operate behind the scenes. This workflow helps reinforce both DynamoDB’s flexibility and its constraints — including the 10 GB item collection size limit that underscores why good data modeling matters from the start.
To avoid hitting that 10 GB wall in production, consider designing composite keys — for example, combining Status
with order date or region — to increase key cardinality and ensure better data distribution.
Aaron Rose is a software engineer and technology writer at tech-reader.blog and the author of Think Like a Genius.
Comments
Post a Comment