Skip to main content
Technology & EngineeringDatabase Services244 lines

Dynamodb

Build with Amazon DynamoDB as a serverless NoSQL database. Use this skill when

Quick Summary26 lines
You are a database specialist who integrates DynamoDB into projects. DynamoDB is
a fully managed, serverless NoSQL database from AWS that provides single-digit
millisecond performance at any scale with automatic scaling and zero maintenance.

## Key Points

- Design for access patterns first — list every query before creating the table
- Use single-table design — one table with PK/SK patterns for all entities
- Use GSIs to support additional query patterns
- Use `begins_with` on sort keys for hierarchical queries
- Use conditional writes to prevent race conditions
- Use TTL for auto-expiring data (sessions, logs, temporary records)
- Use DynamoDB Streams for event-driven architectures
- Designing tables like relational databases — one table per entity
- Using Scan instead of Query — scans read the entire table
- Not planning GSIs upfront — retrofitting access patterns is expensive
- Using FilterExpression instead of proper key design — filters run after reads
- Storing large items (>400KB) — use S3 for large objects

## Quick Example

```bash
npm install @aws-sdk/client-dynamodb @aws-sdk/lib-dynamodb
```
skilldb get database-services-skills/DynamodbFull skill: 244 lines
Paste into your CLAUDE.md or agent config

Amazon DynamoDB Integration

You are a database specialist who integrates DynamoDB into projects. DynamoDB is a fully managed, serverless NoSQL database from AWS that provides single-digit millisecond performance at any scale with automatic scaling and zero maintenance.

Core Philosophy

Single-table design

DynamoDB works best when you put all your data in one table with carefully designed partition and sort keys. This isn't like relational design — you model your data around your access patterns, not your entities.

Access patterns first

Before creating a table, list every query your application needs. Design your keys and indexes to serve those queries. You cannot query DynamoDB flexibly — if you didn't plan for a query, you can't do it without a full table scan.

No joins, no aggregations

DynamoDB doesn't support joins or server-side aggregations. If you need them, denormalize your data or compute aggregates in your application. This is the trade-off for unlimited scale.

Setup

Install

npm install @aws-sdk/client-dynamodb @aws-sdk/lib-dynamodb

Initialize

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

const client = new DynamoDBClient({ region: process.env.AWS_REGION });
const docClient = DynamoDBDocumentClient.from(client, {
  marshallOptions: { removeUndefinedValues: true },
});

Key Techniques

Single-table design

// All entities in one table with PK/SK patterns
// PK             | SK                | Attributes
// USER#alice     | PROFILE           | name, email, plan
// USER#alice     | POST#2024-01-15   | title, content, status
// USER#alice     | APIKEY#key123     | key, active, usageCount
// POST#uuid      | METADATA          | title, authorId, status
// POST#uuid      | COMMENT#timestamp | text, authorId

const TABLE = process.env.TABLE_NAME!;

CRUD operations

import { GetCommand, PutCommand, UpdateCommand, DeleteCommand, QueryCommand } from '@aws-sdk/lib-dynamodb';

// Put (create or overwrite)
await docClient.send(new PutCommand({
  TableName: TABLE,
  Item: {
    PK: `USER#${userId}`,
    SK: 'PROFILE',
    name: 'Alice',
    email: 'alice@example.com',
    plan: 'free',
    createdAt: new Date().toISOString(),
    GSI1PK: 'USERS',
    GSI1SK: `USER#${userId}`,
  },
}));

// Get (exact key lookup)
const { Item } = await docClient.send(new GetCommand({
  TableName: TABLE,
  Key: { PK: `USER#${userId}`, SK: 'PROFILE' },
}));

// Query (partition key + sort key condition)
const { Items } = await docClient.send(new QueryCommand({
  TableName: TABLE,
  KeyConditionExpression: 'PK = :pk AND begins_with(SK, :sk)',
  ExpressionAttributeValues: {
    ':pk': `USER#${userId}`,
    ':sk': 'POST#',
  },
  ScanIndexForward: false,  // Descending order
  Limit: 20,
}));

// Query with filter
const { Items: activePosts } = await docClient.send(new QueryCommand({
  TableName: TABLE,
  KeyConditionExpression: 'PK = :pk AND begins_with(SK, :sk)',
  FilterExpression: '#status = :status',
  ExpressionAttributeNames: { '#status': 'status' },
  ExpressionAttributeValues: {
    ':pk': `USER#${userId}`,
    ':sk': 'POST#',
    ':status': 'published',
  },
}));

// Update
await docClient.send(new UpdateCommand({
  TableName: TABLE,
  Key: { PK: `USER#${userId}`, SK: 'PROFILE' },
  UpdateExpression: 'SET #name = :name, updatedAt = :now',
  ExpressionAttributeNames: { '#name': 'name' },
  ExpressionAttributeValues: {
    ':name': 'Alice Updated',
    ':now': new Date().toISOString(),
  },
}));

// Conditional update
await docClient.send(new UpdateCommand({
  TableName: TABLE,
  Key: { PK: `USER#${userId}`, SK: 'PROFILE' },
  UpdateExpression: 'SET plan = :newPlan',
  ConditionExpression: 'plan = :currentPlan',
  ExpressionAttributeValues: {
    ':newPlan': 'pro',
    ':currentPlan': 'free',
  },
}));

// Delete
await docClient.send(new DeleteCommand({
  TableName: TABLE,
  Key: { PK: `USER#${userId}`, SK: 'PROFILE' },
}));

// Atomic counter
await docClient.send(new UpdateCommand({
  TableName: TABLE,
  Key: { PK: `POST#${postId}`, SK: 'METADATA' },
  UpdateExpression: 'ADD viewCount :inc',
  ExpressionAttributeValues: { ':inc': 1 },
}));

Global Secondary Index (GSI) queries

// Query GSI — e.g., find all users
const { Items } = await docClient.send(new QueryCommand({
  TableName: TABLE,
  IndexName: 'GSI1',
  KeyConditionExpression: 'GSI1PK = :pk',
  ExpressionAttributeValues: { ':pk': 'USERS' },
}));

// Query by email (GSI2 with email as key)
const { Items: byEmail } = await docClient.send(new QueryCommand({
  TableName: TABLE,
  IndexName: 'GSI2',
  KeyConditionExpression: 'GSI2PK = :email',
  ExpressionAttributeValues: { ':email': `EMAIL#${email}` },
}));

Transactions

import { TransactWriteCommand } from '@aws-sdk/lib-dynamodb';

await docClient.send(new TransactWriteCommand({
  TransactItems: [
    {
      Update: {
        TableName: TABLE,
        Key: { PK: `POST#${postId}`, SK: 'METADATA' },
        UpdateExpression: 'SET #status = :status',
        ExpressionAttributeNames: { '#status': 'status' },
        ExpressionAttributeValues: { ':status': 'published' },
      },
    },
    {
      Put: {
        TableName: TABLE,
        Item: {
          PK: `USER#${authorId}`,
          SK: `NOTIFICATION#${Date.now()}`,
          type: 'post_published',
          postId,
        },
      },
    },
  ],
}));

Pagination

async function* paginate(params: QueryCommandInput) {
  let lastKey: Record<string, any> | undefined;
  do {
    const { Items, LastEvaluatedKey } = await docClient.send(
      new QueryCommand({ ...params, ExclusiveStartKey: lastKey })
    );
    yield Items ?? [];
    lastKey = LastEvaluatedKey;
  } while (lastKey);
}

for await (const page of paginate({ TableName: TABLE, KeyConditionExpression: 'PK = :pk', ExpressionAttributeValues: { ':pk': 'USERS' }, IndexName: 'GSI1' })) {
  for (const item of page) {
    console.log(item);
  }
}

Best Practices

  • Design for access patterns first — list every query before creating the table
  • Use single-table design — one table with PK/SK patterns for all entities
  • Use GSIs to support additional query patterns
  • Use begins_with on sort keys for hierarchical queries
  • Use conditional writes to prevent race conditions
  • Use TTL for auto-expiring data (sessions, logs, temporary records)
  • Use DynamoDB Streams for event-driven architectures

Anti-Patterns

  • Designing tables like relational databases — one table per entity
  • Using Scan instead of Query — scans read the entire table
  • Not planning GSIs upfront — retrofitting access patterns is expensive
  • Using FilterExpression instead of proper key design — filters run after reads
  • Storing large items (>400KB) — use S3 for large objects
  • Hot partition keys — distribute writes across partitions
  • Not handling pagination — DynamoDB returns max 1MB per query

Install this skill directly: skilldb add database-services-skills

Get CLI access →