All topics
Database · Learning hub

DynamoDB notes for developers

Master DynamoDB with a curated set of 3 developer notes — core concepts, patterns, and interview prep. Maintained by the DevRecall team.

Save this stack to your DevRecallMore Database notes
DynamoDB

Data Modeling & Core Concepts

DynamoDB: Data Modeling & Core Concepts DynamoDB is AWS's fully managed NoSQL key-value and document database. It provides single-digit millisecond latency at a

DynamoDB: Data Modeling & Core Concepts

DynamoDB is AWS's fully managed NoSQL key-value and document database. It provides single-digit millisecond latency at any scale with automatic horizontal scaling. The data model is fundamentally different from relational databases.

Key Concepts

  • Table: collection of items (equivalent to a SQL table but schema-less)

  • Item: a single record — like a JSON object, up to 400KB

  • Attribute: a field within an item — can be scalar, set, list, map, or binary

  • Primary Key: uniquely identifies an item — either simple (Partition Key only) or composite (PK + Sort Key)

  • Partition Key (PK / HASH key): determines which partition stores the item — must distribute evenly to avoid hot partitions

  • Sort Key (SK / RANGE key): secondary part of composite key — enables range queries within a partition

  • Single-table design: store all entities in one table using PK/SK patterns — the recommended approach for most DynamoDB apps

Primary Key Design

The most important design decision in DynamoDB. Poor key design causes hot partitions (throttling) and missing access patterns.

Single-table design example — e-commerce app

PK              SK                  Attributes
-----------     -----------------   ------------------------------------
USER#alice      PROFILE             name, email, createdAt
USER#alice      ORDER#2026-001      total, status, items
USER#alice      ORDER#2026-002      total, status, items
PRODUCT#widget  METADATA            name, price, stock
PRODUCT#widget  REVIEW#alice        rating, comment
ORDER#2026-001  ITEM#1              productId, qty, price

Access patterns this supports:
  - Get user profile:        PK=USER#alice, SK=PROFILE
  - Get all orders for user: PK=USER#alice, SK begins_with ORDER#
  - Get single order:        PK=USER#alice, SK=ORDER#2026-001
  - Get product metadata:    PK=PRODUCT#widget, SK=METADATA
  - Get product reviews:     PK=PRODUCT#widget, SK begins_with REVIEW#

Capacity Modes

  • On-demand: pay per request (read/write units) — no capacity planning, auto-scales instantly

  • Provisioned: specify RCUs and WCUs in advance — cheaper at predictable load, can use auto-scaling

  • RCU (Read Capacity Unit): 1 strongly consistent read of ≤4KB, or 2 eventually consistent reads

  • WCU (Write Capacity Unit): 1 write of ≤1KB per second

  • Burst capacity: DynamoDB retains up to 5 minutes of unused capacity for bursts

  • Hot partitions: if one partition key receives >1000 WCU or 3000 RCU, it throttles — design PKs to distribute load

Consistency Models

  • Eventually consistent reads (default): may return stale data — costs 0.5 RCU per 4KB

  • Strongly consistent reads: always returns latest data — costs 1 RCU per 4KB, higher latency

  • Transactional reads/writes: ACID guarantees across multiple items — costs 2x RCU/WCU

  • Use strong consistency for: financial data, inventory, any case where stale data causes bugs

  • Use eventual consistency for: read-heavy dashboards, feeds, analytics

DynamoDB

CRUD Operations & Queries

DynamoDB: CRUD Operations & Queries AWS SDK v3 — Node.js import { DynamoDBClient } from '@aws-sdk/client-dynamodb' import { DynamoDBDocumentClient, GetCommand,

DynamoDB: CRUD Operations & Queries

AWS SDK v3 — Node.js

import { DynamoDBClient } from '@aws-sdk/client-dynamodb'
import { DynamoDBDocumentClient, GetCommand, PutCommand, UpdateCommand,
         DeleteCommand, QueryCommand, ScanCommand, TransactWriteCommand } from '@aws-sdk/lib-dynamodb'

const client = new DynamoDBClient({ region: 'eu-west-1' })
const db = DynamoDBDocumentClient.from(client)
const TABLE = 'MyApp'

GetItem & PutItem

// GetItem — fetch by exact primary key
const { Item } = await db.send(new GetCommand({
  TableName: TABLE,
  Key: { PK: 'USER#alice', SK: 'PROFILE' },
  ConsistentRead: true,  // strong consistency
}))

// PutItem — create or overwrite
await db.send(new PutCommand({
  TableName: TABLE,
  Item: {
    PK: 'USER#alice',
    SK: 'PROFILE',
    name: 'Alice',
    email: 'alice@example.com',
    createdAt: new Date().toISOString(),
  },
  // Prevent overwrite if item already exists
  ConditionExpression: 'attribute_not_exists(PK)',
}))

// UpdateItem — modify specific attributes without overwriting whole item
await db.send(new UpdateCommand({
  TableName: TABLE,
  Key: { PK: 'USER#alice', SK: 'PROFILE' },
  UpdateExpression: 'SET #name = :name, updatedAt = :ts ADD loginCount :one',
  ExpressionAttributeNames: { '#name': 'name' },  // 'name' is a reserved word
  ExpressionAttributeValues: {
    ':name': 'Alice Smith',
    ':ts': new Date().toISOString(),
    ':one': 1,
  },
}))

Query & Scan

// Query — efficient, uses key conditions (PK required)
const { Items } = await db.send(new QueryCommand({
  TableName: TABLE,
  KeyConditionExpression: 'PK = :pk AND begins_with(SK, :prefix)',
  FilterExpression: '#status = :active',   // applied after key filter — still reads all matching keys
  ExpressionAttributeNames: { '#status': 'status' },
  ExpressionAttributeValues: {
    ':pk': 'USER#alice',
    ':prefix': 'ORDER#',
    ':active': 'active',
  },
  ScanIndexForward: false,  // DESC order by sort key
  Limit: 10,                // max items per page
}))

// Paginate — LastEvaluatedKey signals more pages exist
async function queryAll(pk: string) {
  const items = []
  let lastKey
  do {
    const { Items: page, LastEvaluatedKey } = await db.send(new QueryCommand({
      TableName: TABLE,
      KeyConditionExpression: 'PK = :pk',
      ExpressionAttributeValues: { ':pk': pk },
      ExclusiveStartKey: lastKey,
    }))
    items.push(...(page ?? []))
    lastKey = LastEvaluatedKey
  } while (lastKey)
  return items
}

// Scan — reads entire table (avoid in production unless necessary)
const { Items: all } = await db.send(new ScanCommand({
  TableName: TABLE,
  FilterExpression: 'contains(tags, :tag)',
  ExpressionAttributeValues: { ':tag': 'featured' },
}))

Transactions

// TransactWrite — all-or-nothing across up to 100 items in one table (or multiple)
await db.send(new TransactWriteCommand({
  TransactItems: [
    {
      Update: {
        TableName: TABLE,
        Key: { PK: 'PRODUCT#widget', SK: 'METADATA' },
        UpdateExpression: 'ADD stock :minus',
        ConditionExpression: 'stock >= :qty',
        ExpressionAttributeValues: { ':minus': -1, ':qty': 1 },
      }
    },
    {
      Put: {
        TableName: TABLE,
        Item: {
          PK: 'USER#alice',
          SK: 'ORDER#2026-999',
          productId: 'widget',
          qty: 1,
          status: 'pending',
        },
        ConditionExpression: 'attribute_not_exists(PK)',
      }
    }
  ]
}))
DynamoDB

GSIs, Streams, DynamoDB Local & Interview Questions

DynamoDB: GSIs, Streams & Interview Questions Global Secondary Indexes (GSI) GSIs let you query on non-primary-key attributes. Each GSI has its own PK and optio

DynamoDB: GSIs, Streams & Interview Questions

Global Secondary Indexes (GSI)

GSIs let you query on non-primary-key attributes. Each GSI has its own PK and optional SK, and maintains its own copy of the data — eventually consistent by default.

// Table: PK=USER#id, SK=ORDER#date
// Add GSI to query all orders by status (cross-user)
// GSI: PK=status, SK=createdAt

// Query the GSI
const { Items } = await db.send(new QueryCommand({
  TableName: TABLE,
  IndexName: 'GSI1',        // GSI name set during table creation
  KeyConditionExpression: 'GSI1PK = :status AND GSI1SK > :since',
  ExpressionAttributeValues: {
    ':status': 'ORDER_STATUS#pending',
    ':since': '2026-01-01',
  },
}))
  • Up to 20 GSIs per table (Local Secondary Indexes also available — same PK, different SK)

  • GSI consumes its own RCU/WCU — provision separately or use on-demand

  • Sparse indexes: only items with the GSI key attributes are indexed — useful for filtering

  • GSI overloading: reuse the same GSI for multiple entity types by putting entity-type prefixes in GSI keys

DynamoDB Streams

Streams capture a time-ordered sequence of item-level changes (INSERT, MODIFY, REMOVE). Use them to trigger Lambda functions, replicate data, or build event-driven systems.

// Enable on table: StreamSpecification = KEYS_ONLY | NEW_IMAGE | OLD_IMAGE | NEW_AND_OLD_IMAGES

// Lambda trigger automatically invoked with stream records
export const handler = async (event: DynamoDBStreamEvent) => {
  for (const record of event.Records) {
    if (record.eventName === 'INSERT') {
      const newItem = record.dynamodb?.NewImage
      // newItem attributes are in DynamoDB AttributeValue format
      // Use unmarshall from @aws-sdk/util-dynamodb to convert
    }
    if (record.eventName === 'MODIFY') {
      const old = record.dynamodb?.OldImage
      const updated = record.dynamodb?.NewImage
    }
    if (record.eventName === 'REMOVE') {
      const deleted = record.dynamodb?.OldImage
    }
  }
}

DynamoDB Local (Development)

# Run DynamoDB locally via Docker
docker run -p 8000:8000 amazon/dynamodb-local

# Point SDK to local instance
const client = new DynamoDBClient({
  region: 'local',
  endpoint: 'http://localhost:8000',
  credentials: { accessKeyId: 'fake', secretAccessKey: 'fake' }
})

# CLI with local
aws dynamodb list-tables --endpoint-url http://localhost:8000 --region us-east-1

Interview Questions

  • When to choose DynamoDB over PostgreSQL? Known access patterns, massive scale (millions of requests/sec), serverless/event-driven architecture. Avoid when: complex queries, ad-hoc reporting, many-to-many joins.

  • What is a hot partition and how do you avoid it? Single partition receiving too many requests. Fix: add random suffix to PK (write sharding), use higher-cardinality PKs, cache hot reads in ElastiCache.

  • Difference between Query and Scan? Query uses the index (efficient, O(results)); Scan reads the entire table (expensive, avoid). Always prefer Query with proper key design.

  • What is the maximum item size in DynamoDB? 400KB. For larger items, store in S3 and save the S3 key in DynamoDB.

  • How does single-table design differ from multi-table? Single-table stores all entities in one table with generic PK/SK — enables fetching related data in one Query. Multi-table is easier to reason about but requires multiple queries for related data.

  • How do you handle DynamoDB transactions? TransactWrite/TransactGet (up to 100 items, 4MB) — 2x cost. For high-volume: optimistic locking with ConditionExpression instead.

Keep your DynamoDB knowledge sharp.

Save this stack to your personal DevRecall — add your own notes, track what you're learning, and share what you know with the community.

Get started — free forever