Skip to content

DynamoDB Cheat Sheet

DynamoDB is a key-value and document store where access patterns drive table design: partition key (PK), optional sort key (SK), and GSIs for alternate query paths.

How to use this DynamoDB cheat sheet

GetItem fetches one item by full primary key; Query reads items sharing a partition key with optional SK conditions. Scan reads entire table—avoid in production except migrations or analytics with filters.

AWS SDK v3 uses command objects (GetItemCommand, QueryCommand) with DynamoDBClient. On-demand vs provisioned capacity affects cost; single-table design colocates related entities with composite keys and overloaded GSIs.

Quick DynamoDB example

import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
import { DynamoDBDocumentClient, GetCommand, QueryCommand } from '@aws-sdk/lib-dynamodb';

const client = DynamoDBDocumentClient.from(new DynamoDBClient({ region: 'us-east-1' }));

const user = await client.send(new GetCommand({
  TableName: 'App',
  Key: { PK: 'USER#123', SK: 'PROFILE' },
}));

const orders = await client.send(new QueryCommand({
  TableName: 'App',
  KeyConditionExpression: 'PK = :pk AND begins_with(SK, :sk)',
  ExpressionAttributeValues: { ':pk': 'USER#123', ':sk': 'ORDER#' },
}));

PK/SK & Access Patterns

Concept Example Notes
Partition key PK: "USER#123" Hash key; spreads load across partitions
Sort key SK: "ORDER#2024-01-01" Range key; enables begins_with queries
Composite key { PK: "USER#1", SK: "PROFILE" } Uniquely identifies one item
Single-table PK=USER#1, SK=ORDER#5 same table as profiles Multiple entity types via key prefixes
Hot partition High write rate to one PK Salting or write sharding spreads load
Item size limit 400 KB per item Split large blobs to S3 with pointer attribute
TTL ttl: epochSeconds Automatic expiry without billing for storage after delete
ConditionExpression attribute_not_exists(PK) Optimistic locking and idempotent writes

GetItem, Query & Scan

Operation Example When to use
GetItem GetCommand({ Key: { PK, SK } }) Single item by full primary key
Query KeyConditionExpression: "PK = :pk" All items under one partition
Query + SK PK = :pk AND SK BETWEEN :a AND :b Range within partition
begins_with begins_with(SK, :prefix) Prefix scan within partition only
FilterExpression FilterExpression: "amount > :min" Applied after read; still consumes RCUs
Scan ScanCommand({ TableName }) Full table read—expensive, slow
BatchGetItem Keys: [{ PK, SK }, ...] Up to 100 items; 16 MB response limit
TransactWrite TransactWriteCommand All-or-nothing up to 100 actions

GSI & SDK v3 Commands

Feature Example Notes
GSI GSI1PK, GSI1SK on same item Alternate query access pattern
Sparse GSI Only items with GSI keys appear Index subset without filter waste
Projection ProjectionType: "INCLUDE" Copy only needed attributes to GSI
PutItem PutCommand({ Item: { PK, SK, ...attrs } }) Create or replace entire item
UpdateItem UpdateExpression: "SET #s = :val" Partial update with ExpressionAttributeNames
DeleteItem DeleteCommand({ Key }) Remove single item
DocumentClient DynamoDBDocumentClient.from(client) Native JS types instead of AttributeValue maps
marshall/unmarshall marshall({ foo: "bar" }) Low-level AttributeValue conversion

Capacity & Single-Table Patterns

Topic Example Notes
On-demand BillingMode: "PAY_PER_REQUEST" Pay per request; good for variable traffic
Provisioned ReadCapacityUnits, WriteCapacityUnits Predictable cost at steady load
RCU/WCU 1 RCU = 4 KB strongly consistent read Eventually consistent reads half RCU
Entity collection PK=USER#1, SK=PROFILE|ORDER#|INVOICE# Query all user data with PK only
Adjacency list Multiple SK types under one PK Graph-like one-hop relationships
Inverted index GSI GSI1PK=ORDER#5, GSI1SK=USER#1 Lookup orders by order id
Write sharding PK=USER#1#SHARD@@CODE0@@#123;hash % 10} Reduce hot partition on writes
Idempotency ConditionExpression: attribute_not_exists(PK) Prevent duplicate event processing

UpdateItem with optimistic locking

await client.send(new UpdateCommand({
  TableName: 'App',
  Key: { PK: 'USER#1', SK: 'PROFILE' },
  UpdateExpression: 'SET #v = :newV, #version = :next',
  ConditionExpression: '#version = :current',
  ExpressionAttributeNames: { '#v': 'name', '#version': 'version' },
  ExpressionAttributeValues: { ':newV': 'Ada', ':next': 2, ':current': 1 },
}));

ConditionExpression fails with ConditionalCheckFailedException if version mismatch.

Batch write with retry unprocessed

import { BatchWriteCommand } from '@aws-sdk/lib-dynamodb';
let request = { RequestItems: { App: puts } };
do {
  const res = await client.send(new BatchWriteCommand(request));
  request = { RequestItems: res.UnprocessedItems };
} while (Object.keys(request.RequestItems).length);

Exponential backoff on UnprocessedItems for throttling.

Single-table entity types

// Same table, different entities
{ PK: 'USER#42', SK: 'PROFILE', name: 'Ada', type: 'User' }
{ PK: 'USER#42', SK: 'ORDER#1001', total: 99, type: 'Order' }
{ PK: 'ORDER#1001', SK: 'METADATA', status: 'SHIPPED', type: 'OrderMeta' }

Design keys for every query before creating the table—GSIs are costly to change later.

Common mistakes

  • Using Scan where Query with a designed PK would suffice.
  • FilterExpression thinking it reduces RCUs—it filters after the read.
  • Creating a GSI for every ad-hoc query instead of modeling access patterns upfront.
  • Storing large blobs in DynamoDB instead of S3 with a pointer attribute.

Key takeaways

  • Design PK/SK and GSIs from access patterns, not from relational table shapes.
  • Prefer Query and GetItem; treat Scan as a last resort.
  • Use DocumentClient and SDK v3 commands for cleaner Node.js code.
  • Single-table design with prefixed keys keeps related data queryable in one round trip.

Pro Tip

Sketch every read/write your app needs on paper first—changing keys after launch requires painful migrations and GSI rebuilds.