Claude Code for DynamoDB: Single-Table Design and Access Patterns — Claude Skills 360 Blog
Blog / Backend / Claude Code for DynamoDB: Single-Table Design and Access Patterns
Backend

Claude Code for DynamoDB: Single-Table Design and Access Patterns

Published: November 13, 2026
Read time: 9 min read
By: Claude Skills 360

DynamoDB differs from every other database: you design the table around your access patterns, not your normalized data model. Primary keys are your only indexes — everything else is a full scan. The trick is composite sort keys and GSIs that encode multiple lookup patterns into one table. Done right, DynamoDB scales infinitely with single-digit millisecond latency and zero database administration. Claude Code designs single-table schemas, writes condition expressions that prevent race conditions, and implements DynamoDB Streams processors.

CLAUDE.md for DynamoDB Projects

## DynamoDB Design
- Single-table design: all entity types in one table (avoids cross-table transactions)
- PK: entity type prefix (ORDER#id, USER#id, PRODUCT#id)
- SK: access pattern (METADATA, ORDER#date#id, STATUS#shipped)
- GSIs: up to 20 per table; name by access pattern (GSI-by-user-id, GSI-by-status)
- Condition expressions: enforce constraints atomically (stock > 0, version matches)
- No scans: every query uses PK or GSI — never table scans in production
- DynamoDB Streams: CDC for downstream listeners; process with Lambda

Single-Table Schema Design

Table: myapp-production

Entity: Order
  PK = "ORDER#<orderId>"
  SK = "METADATA"
  GSI1PK = "USER#<userId>"     → Query all orders for a user
  GSI1SK = "ORDER#<createdAt>#<orderId>"  → Time-ordered
  GSI2PK = "STATUS#<status>"   → Query by status (admin dashboard)
  GSI2SK = "ORDER#<createdAt>"

Entity: OrderItem  
  PK = "ORDER#<orderId>"
  SK = "ITEM#<itemId>"

Entity: User
  PK = "USER#<userId>"
  SK = "METADATA"

Access Patterns:
  1. Get order by ID          → Query PK="ORDER#id", SK="METADATA"
  2. Get all items for order  → Query PK="ORDER#id", SK begins_with "ITEM#"
  3. Get all orders for user  → Query GSI1, PK="USER#id" (sorted by time)
  4. Get orders by status     → Query GSI2, PK="STATUS#pending"

DynamoDB Client Setup

// db/dynamodb.ts
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
import { DynamoDBDocumentClient, GetCommand, PutCommand, QueryCommand, UpdateCommand, TransactWriteCommand } from '@aws-sdk/lib-dynamodb';

const client = new DynamoDBClient({ region: process.env.AWS_REGION ?? 'us-east-1' });
export const ddb = DynamoDBDocumentClient.from(client, {
  marshallOptions: { removeUndefinedValues: true },
});

export const TABLE = process.env.DYNAMODB_TABLE ?? 'myapp-production';

// Helper: build keys with type prefix
export const keys = {
  order: (id: string) => ({ PK: `ORDER#${id}`, SK: 'METADATA' }),
  orderItems: (orderId: string) => ({ PK: `ORDER#${orderId}`, SK: 'ITEM#' }),
  user: (id: string) => ({ PK: `USER#${id}`, SK: 'METADATA' }),
};

Core Operations

// db/orders.ts
import { ddb, TABLE, keys } from './dynamodb';
import { GetCommand, PutCommand, QueryCommand, UpdateCommand } from '@aws-sdk/lib-dynamodb';

export async function getOrder(orderId: string): Promise<Order | null> {
  const { Item } = await ddb.send(new GetCommand({
    TableName: TABLE,
    Key: keys.order(orderId),
  }));
  
  return Item ? (Item as Order) : null;
}

export async function createOrder(order: Order): Promise<void> {
  const now = new Date().toISOString();
  
  await ddb.send(new PutCommand({
    TableName: TABLE,
    Item: {
      PK: `ORDER#${order.id}`,
      SK: 'METADATA',
      GSI1PK: `USER#${order.userId}`,
      GSI1SK: `ORDER#${now}#${order.id}`,
      GSI2PK: `STATUS#${order.status}`,
      GSI2SK: `ORDER#${now}`,
      ...order,
      createdAt: now,
      updatedAt: now,
      version: 1,
    },
    // Condition: prevent re-creation with same ID
    ConditionExpression: 'attribute_not_exists(PK)',
  }));
}

// Query all orders for a user (using GSI1)
export async function getOrdersByUser(userId: string, limit = 20): Promise<Order[]> {
  const { Items = [] } = await ddb.send(new QueryCommand({
    TableName: TABLE,
    IndexName: 'GSI1',
    KeyConditionExpression: 'GSI1PK = :pk AND begins_with(GSI1SK, :sk)',
    ExpressionAttributeValues: {
      ':pk': `USER#${userId}`,
      ':sk': 'ORDER#',
    },
    ScanIndexForward: false,  // Descending (newest first)
    Limit: limit,
  }));
  
  return Items as Order[];
}

// Optimistic locking: only update if version matches
export async function updateOrderStatus(
  orderId: string,
  newStatus: string,
  expectedVersion: number,
): Promise<void> {
  try {
    await ddb.send(new UpdateCommand({
      TableName: TABLE,
      Key: keys.order(orderId),
      UpdateExpression: 'SET #status = :status, GSI2PK = :gsi2pk, version = :v, updatedAt = :now',
      ConditionExpression: 'version = :expectedVersion',
      ExpressionAttributeNames: { '#status': 'status' },
      ExpressionAttributeValues: {
        ':status': newStatus,
        ':gsi2pk': `STATUS#${newStatus}`,
        ':v': expectedVersion + 1,
        ':expectedVersion': expectedVersion,
        ':now': new Date().toISOString(),
      },
    }));
  } catch (err) {
    if (err.name === 'ConditionalCheckFailedException') {
      throw new Error('Concurrent modification — retry');
    }
    throw err;
  }
}

DynamoDB Transactions

// db/place-order.ts — atomic order creation with inventory check
export async function placeOrderWithInventory(
  order: Order,
  items: CartItem[],
): Promise<void> {
  // Build transaction: deduct inventory AND create order atomically
  const transactItems = [
    // Create order
    {
      Put: {
        TableName: TABLE,
        Item: buildOrderItem(order),
        ConditionExpression: 'attribute_not_exists(PK)',
      },
    },
    // Deduct inventory for each item
    ...items.map(item => ({
      Update: {
        TableName: TABLE,
        Key: { PK: `PRODUCT#${item.productId}`, SK: 'INVENTORY' },
        UpdateExpression: 'SET stock = stock - :qty',
        ConditionExpression: 'stock >= :qty',  // Prevent negative stock
        ExpressionAttributeValues: {
          ':qty': item.quantity,
        },
      },
    })),
  ];
  
  try {
    await ddb.send(new TransactWriteCommand({ TransactItems: transactItems }));
  } catch (err) {
    if (err.name === 'TransactionCanceledException') {
      // Check which condition failed
      const reasons = err.CancellationReasons;
      if (reasons?.some((r: any) => r.Code === 'ConditionalCheckFailed')) {
        throw new Error('Insufficient inventory for one or more items');
      }
    }
    throw err;
  }
}

DynamoDB Streams + Lambda

// lambda/streams-processor.ts
import type { DynamoDBStreamEvent } from 'aws-lambda';
import { unmarshall } from '@aws-sdk/util-dynamodb';

export async function handler(event: DynamoDBStreamEvent) {
  for (const record of event.Records) {
    if (record.eventName === 'INSERT' && record.dynamodb?.NewImage) {
      const item = unmarshall(record.dynamodb.NewImage as any);
      
      // Only process order creation events
      if (item.PK?.startsWith('ORDER#') && item.SK === 'METADATA') {
        await notifyOrderCreated(item as Order);
      }
    }
    
    if (record.eventName === 'MODIFY' && record.dynamodb?.NewImage) {
      const newItem = unmarshall(record.dynamodb.NewImage as any);
      const oldItem = record.dynamodb.OldImage ? unmarshall(record.dynamodb.OldImage as any) : null;
      
      if (newItem.PK?.startsWith('ORDER#') && newItem.status !== oldItem?.status) {
        await handleOrderStatusChange(newItem as Order, oldItem?.status);
      }
    }
  }
}

For the PostgreSQL alternative when you need flexible queries rather than key-value access patterns, the PostgreSQL advanced guide covers indexing strategies for relational workloads. For the AWS CDK infrastructure that provisions DynamoDB tables with streams and Lambda processors, the AWS CDK guide covers L2 DynamoDB constructs. The Claude Skills 360 bundle includes DynamoDB skill sets covering single-table design, GSI patterns, condition expressions, and Streams processing. Start with the free tier to try DynamoDB schema design generation.

Keep Reading

Backend

Claude Code for Bun: Fast JavaScript Runtime and Toolkit

Build with Bun and Claude Code — Bun.serve for HTTP servers, Bun.file for fast file I/O, Bun.$ for shell commands, Bun.sql for SQLite and PostgreSQL, Bun.build for bundling, bun:test for testing, Bun.hash for hashing, bun.lock for deterministic installs, bun run for package.json scripts, hot reloading with --hot, bun init for project scaffolding, and compatibility with Node.js modules.

6 min read Jun 13, 2027
Backend

Claude Code for Express.js Advanced: Patterns for Production APIs

Advanced Express.js patterns with Claude Code — typed request handlers with RequestHandler generics, async error handling middleware, Zod validation middleware factory, rate limiting with express-rate-limit and Redis store, helmet security middleware, compression, dependency injection with tsyringe, file upload with multer and S3, pagination utilities, JWT middleware, and structured logging with pino.

6 min read Jun 8, 2027
Backend

Claude Code for KeystoneJS: Node.js CMS and App Framework

Build full-stack apps with KeystoneJS and Claude Code — config with lists, fields.text and fields.relationship for schema definition, access control with isAuthenticated and isAdmin functions, hooks with beforeOperation and afterOperation, GraphQL API auto-generation from schema, AdminUI for content management, session with statelessSessions, Prisma adapter for database, file storage with images and files fields, and custom REST endpoints.

6 min read Jun 7, 2027

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free