Skip to main content
Technology & EngineeringDatabase Services243 lines

Dynamodb

Build with Amazon DynamoDB as a serverless NoSQL database. Use this skill when the project needs to integrate DynamoDB for key-value and document storage, single-table design, global tables, streams, TTL, or serverless-first data access. Covers the AWS SDK v3, single-table design, GSIs, queries, scans, transactions, and DynamoDB Streams.

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: 243 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 →

Related Skills

Fauna

Build with Fauna as a distributed document-relational database. Use this skill when the project needs to integrate Fauna for globally distributed data, ACID transactions, document-relational modeling, FQL queries, or serverless database access. Covers the Fauna v10 client, FQL, schema, indexes, and authentication.

Database Services194L

Firebase Firestore

Build with Firebase Firestore as a NoSQL document database. Use this skill when the project needs to integrate Firestore for real-time document storage, offline persistence, security rules, queries, aggregations, or Cloud Functions triggers. Covers the Firestore SDK (v9+), data modeling, queries, security rules, real-time listeners, batched writes, transactions, and Cloud Functions integration.

Database Services266L

Mongodb

Build with MongoDB as a document database. Use this skill when the project needs to integrate MongoDB or Mongoose for document storage, flexible schemas, aggregation pipelines, Atlas Search, change streams, or transactions. Covers the MongoDB Node.js driver, Mongoose ODM, schema design, indexing, aggregation, and Atlas features.

Database Services240L

Neo4j

Build with Neo4j for graph data and relationship-heavy queries. Use this skill when the project needs a graph database for social networks, recommendation engines, knowledge graphs, fraud detection, or any domain where relationships between entities are central. Covers Cypher query language, the Neo4j JavaScript driver, graph modeling, and common graph patterns.

Database Services235L

Neon

Build with Neon as a serverless PostgreSQL database. Use this skill when the project needs to integrate Neon for serverless Postgres, branching, autoscaling, connection pooling, or edge-compatible database access. Covers the Neon serverless driver, branching workflows, connection pooling, and integration with ORMs.

Database Services164L

Planetscale

Build with PlanetScale as a serverless MySQL database. Use this skill when the project needs to integrate PlanetScale for serverless MySQL, branching, non-blocking schema changes, connection pooling, or Vitess-powered horizontal scaling. Covers the PlanetScale serverless driver, branching, deploy requests, and ORM integration.

Database Services155L