DynamoDB is a managed key-value and document database that scales without servers to patch, with single-digit-millisecond reads and writes at almost any size. It rewards you for knowing your access patterns up front and punishes you for treating it like a relational database.
Versions in use on this date. AWS CLI 2.7 and the current DynamoDB feature set: on-demand and provisioned capacity, the Standard-Infrequent Access table class (December 2021), point-in-time recovery, TTL, transactions and export to S3.
Mental model
flowchart TB
APP[Application] -->|PutItem GetItem Query| T
subgraph T[Table Orders]
direction TB
PK[Partition key customerId<br/>hashed to choose partition]
SK[Sort key orderId<br/>orders items inside the partition]
PK --> SK
end
T -->|projects selected attributes| GSI[Global secondary index<br/>status + createdAt]
T --> LSI[Local secondary index<br/>same partition key, other sort key]
T --> STREAM[DynamoDB Streams<br/>change events]
STREAM --> LAMBDA[Lambda consumer]
T --> PITR[Point-in-time recovery<br/>last 35 days]
T --> TTL[TTL attribute deletes expired items]
Every item is found by its primary key: either a partition key alone, or a partition key plus a sort key. The partition key is hashed to place the item; a Query reads one partition key value (optionally a range of sort keys) efficiently, while a Scan reads the whole table and should be rare.
Create, write and read
aws dynamodb create-table --table-name Orders \
--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 --region ap-south-1
aws dynamodb put-item --table-name Orders \
--item '{"customerId":{"S":"c1"},"orderId":{"S":"2022-09-05#o100"},"status":{"S":"PAID"},"total":{"N":"499"}}' \
--condition-expression "attribute_not_exists(orderId)" # do not overwrite an existing order
aws dynamodb query --table-name Orders \
--key-condition-expression "customerId = :c AND begins_with(orderId, :d)" \
--expression-attribute-values '{":c":{"S":"c1"},":d":{"S":"2022-09"}}'
aws dynamodb update-item --table-name Orders \
--key '{"customerId":{"S":"c1"},"orderId":{"S":"2022-09-05#o100"}}' \
--update-expression "SET #s = :new" --condition-expression "#s = :old" \
--expression-attribute-names '{"#s":"status"}' \
--expression-attribute-values '{":new":{"S":"SHIPPED"},":old":{"S":"PAID"}}'
Conditional writes give you optimistic locking and idempotency without extra infrastructure. Note the sort key date#orderId pattern: one query returns a customer’s orders for a month in order.
Design for access patterns
- List the questions the application asks: “orders for a customer”, “open orders by status”, “one order by ID”.
- Choose the primary key to serve the most important pattern directly.
- Add global secondary indexes (GSI) for the others, each with its own key and projected attributes. Indexes are updated asynchronously and cost write capacity and storage.
- Prefer high-cardinality partition keys (customer, device) over low-cardinality ones (
status), otherwise a hot partition throttles you. - Items are limited to 400 KB; store big blobs in S3 and keep a pointer.
Some teams keep several entity types in one table with generic keys (PK, SK) to fetch related items in one query. It is powerful but harder to understand; use it only when you can articulate the patterns it serves.
Capacity modes
| Mode | Behaviour | Choose when |
|---|---|---|
On-demand (PAY_PER_REQUEST) |
Pay per read and write request, no planning | New, spiky or unknown traffic |
| Provisioned | You set read and write capacity units, with optional auto scaling | Steady, predictable traffic; cheaper at scale |
A strongly consistent read of up to 4 KB costs 1 read unit, an eventually consistent read costs half, and a write of up to 1 KB costs 1 write unit. Reads are eventually consistent by default; ask for --consistent-read when you must see your own latest write (not available on global indexes). Consider the Standard-IA table class for large, rarely read tables.
Protect and tidy data
aws dynamodb update-continuous-backups --table-name Orders \
--point-in-time-recovery-specification PointInTimeRecoveryEnabled=true
aws dynamodb update-time-to-live --table-name Orders \
--time-to-live-specification "Enabled=true,AttributeName=expiresAt" # epoch seconds
aws dynamodb update-table --table-name Orders --deletion-protection-enabled
aws dynamodb restore-table-to-point-in-time --source-table-name Orders \
--target-table-name Orders-restore --use-latest-restorable-time
- Point-in-time recovery restores to any second in the last 35 days into a new table; restores take time proportional to size, so rehearse.
- TTL removes expired items for free, typically within a day or so of expiry, so filter expired items in queries if exactness matters.
- Turn on deletion protection for production tables. Encryption at rest is always on.
Operations
- Alarm on
ThrottledRequests,SystemErrorsandConsumedReadCapacityUnitsversus provisioned, and onReplicationLatencyfor global tables. - Use Streams with Lambda for change-driven work (search indexing, audit trails); handle duplicates, since delivery is at least once.
- Use transactions (
TransactWriteItems) for all-or-nothing updates across items, remembering they cost twice the capacity. - Manage tables with Terraform or CloudFormation rather than the console, and never share one table between unrelated services.
Common mistakes
- Using
Scanfor routine queries, which gets slower and costlier as the table grows. - A hot partition key, such as all writes to
date=today. - Modeling relational joins; instead denormalize the data you read together.
- Forgetting pagination:
Queryreturns at most 1 MB, so loop usingLastEvaluatedKey.