Core Concepts

Single-Table Design

Master DynamoDB's most powerful pattern: storing multiple entity types in one table to serve all access patterns efficiently.

What is Single-Table Design?

Single-table design (STD) is a DynamoDB modeling pattern where you store all entity types — users, orders, products, comments — in a single table, using a generic primary key structure (often PK and SK).

This sounds counterintuitive at first, but it's the recommended approach for DynamoDB because:

  1. DynamoDB charges per read/write operation. Joining across tables requires multiple requests.
  2. DynamoDB can return related items in a single request if they share a partition key.
  3. Fewer tables means simpler operations (backup, permissions, monitoring).

The key insight: items with the same PK are stored together on the same partition and can be fetched in a single query.

Generic Key Naming

Instead of userId and orderId, use generic names PK and SK, then store composite values:

text
PK                    SK                    Other attrs
USER#alice            PROFILE               name, email, role
USER#alice            ORDER#2025-001        total, status
USER#alice            ORDER#2025-002        total, status
PRODUCT#sku-123       METADATA              name, price, stock
ORDER#2025-001        ITEM#sku-123          quantity, price

Access Patterns

Define your access patterns before designing the table:

  1. Get user profile → PK = USER#email, SK = PROFILE
  2. Get all orders for user → PK = USER#email, SK begins_with ORDER#
  3. Get single order → PK = USER#email, SK = ORDER#id
  4. Get product → PK = PRODUCT#sku, SK = METADATA
javascript
import { QueryCommand } from '@aws-sdk/lib-dynamodb';

// Pattern 2: All orders for a user
const result = await docClient.send(new QueryCommand({
  TableName: 'MyApp',
  KeyConditionExpression: 'PK = :pk AND begins_with(SK, :sk)',
  ExpressionAttributeValues: {
    ':pk': 'USER#alice@example.com',
    ':sk': 'ORDER#',
  },
}));

// Result includes all order items for alice in one request!
console.log(result.Items);

Global Secondary Indexes (GSI)

For access patterns that don't fit the main table's key structure, add a GSI:

javascript
// Access pattern: "Get all orders with status = PENDING"
// Main table PK/SK won't help here — we need a different key

// GSI: GSI1PK = entity type, GSI1SK = status
// Each item stores:
// GSI1PK = "ORDER"
// GSI1SK = "PENDING#2025-01-15"

const result = await docClient.send(new QueryCommand({
  TableName: 'MyApp',
  IndexName: 'GSI1',
  KeyConditionExpression: 'GSI1PK = :type AND begins_with(GSI1SK, :status)',
  ExpressionAttributeValues: {
    ':type': 'ORDER',
    ':status': 'PENDING#',
  },
}));

Transactions

DynamoDB supports ACID transactions across up to 100 items in a single request:

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

await docClient.send(new TransactWriteCommand({
  TransactItems: [
    {
      Update: {
        TableName: 'MyApp',
        Key: { PK: 'PRODUCT#sku-123', SK: 'METADATA' },
        UpdateExpression: 'SET stock = stock - :qty',
        ConditionExpression: 'stock >= :qty',
        ExpressionAttributeValues: { ':qty': 1 },
      },
    },
    {
      Put: {
        TableName: 'MyApp',
        Item: {
          PK: 'USER#alice',
          SK: 'ORDER#2025-001',
          status: 'CONFIRMED',
          total: 49.99,
        },
      },
    },
  ],
}));

Example

javascript
// Full single-table item management
import { PutCommand, QueryCommand, UpdateCommand } from '@aws-sdk/lib-dynamodb';

// Create user + initial profile in one write
await docClient.send(new PutCommand({
  TableName: 'MyApp',
  Item: {
    PK: 'USER#alice@example.com',
    SK: 'PROFILE',
    entityType: 'USER',
    name: 'Alice Smith',
    email: 'alice@example.com',
    createdAt: new Date().toISOString(),
    GSI1PK: 'USER',
    GSI1SK: 'alice@example.com',
  },
  ConditionExpression: 'attribute_not_exists(PK)', // prevent overwrite
}));

Want to run this code interactively?

Try in Compiler