Happy Dussehra! May good always win. Happy Diwali! Wishing you a bright year ahead. Merry Christmas! Warm wishes from Vizag. Happy New Year! Here’s to a great year ahead.

Archive · AWS

DynamoDB Basics

Tables, keys, indexes, capacity modes, consistency, TTL, point-in-time recovery and single-table design thinking, with CLI examples and common pitfalls.

4 min read

  • Amazon DynamoDB
  • AWS CLI 2.7
  • Region ap-south-1
  • PAY_PER_REQUEST

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

  1. List the questions the application asks: “orders for a customer”, “open orders by status”, “one order by ID”.
  2. Choose the primary key to serve the most important pattern directly.
  3. 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.
  4. Prefer high-cardinality partition keys (customer, device) over low-cardinality ones (status), otherwise a hot partition throttles you.
  5. 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, SystemErrors and ConsumedReadCapacityUnits versus provisioned, and on ReplicationLatency for 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 Scan for 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: Query returns at most 1 MB, so loop using LastEvaluatedKey.