Mình mất khá nhiều thời gian để hiểu tại sao cùng một table query nhanh chỗ này mà chậm chỗ kia, và câu trả lời nằm ở key design. DynamoDB khác RDS tận gốc: không connection (HTTP API), không schema cố định (mỗi item có attribute riêng), không provisioning (On-Demand), scale ngang tự động. Với Lambda + API Gateway, DynamoDB là database tự nhiên nhất: pay-per-request, không connection pool, latency single-digit ms ở mọi scale.

Nhưng sức mạnh đi kèm learning curve: key design, GSI, capacity mode. Single-table nâng cao ở Bài 41.


  flowchart TB
    Table["DynamoDB Table"]
    PK["Partition Key (PK)<br/>quyết định partition vật lý"]
    SK["Sort Key (SK)<br/>range query trong partition"]
    GSI["GSI<br/>query theo key khác<br/>eventual consistency"]
    Stream["DynamoDB Stream<br/>capture mọi thay đổi<br/>→ Lambda trigger"]
    Table --> PK
    Table --> SK
    Table --> GSI
    Table --> Stream

Key design: PK quyết định partition

import {
  DynamoDBDocumentClient,
  PutCommand,
  QueryCommand,
} from "@aws-sdk/lib-dynamodb";
import { unmarshall } from "@aws-sdk/util-dynamodb";

const ddb = DynamoDBDocumentClient.from(new DynamoDBClient({}));

// Put item — PK + SK xác định vị trí duy nhất
await ddb.send(
  new PutCommand({
    TableName: "myapp",
    Item: {
      PK: "USER#alice",
      SK: "PROFILE#main",
      name: "Alice",
      email: "[email protected]",
      team: "backend",
    },
  })
);

// Query theo PK — lấy tất cả item của user
const { Items } = await ddb.send(
  new QueryCommand({
    TableName: "myapp",
    KeyConditionExpression: "PK = :pk AND begins_with(SK, :sk)",
    ExpressionAttributeValues: { ":pk": "USER#alice", ":sk": "TASK#" },
  })
);

Capacity modes

On-DemandProvisioned
Giá$1.25/1M WRU$0.00065/WCU-hr (~$0.47/1M)
ThrottleHiếm (spike >2x peak)Có nếu traffic > capacity
Dùng khiTraffic unpredictable, devTraffic ổn định, cost-sensitive

Bắt đầu On-Demand → chuyển Provisioned khi hiểu access pattern. Switch được bất kỳ lúc nào.


GSI + TTL + Stream

// GSI: query theo status (không phải PK chính)
await ddb.send(
  new QueryCommand({
    TableName: "myapp",
    IndexName: "GSI1",
    KeyConditionExpression: "GSI1PK = :status",
    ExpressionAttributeValues: { ":status": "TASK_STATUS#pending" },
  })
);

// TTL: auto-expire session
await ddb.send(
  new PutCommand({
    TableName: "myapp",
    Item: {
      PK: "SESSION#abc",
      SK: "TOKEN#xyz",
      expiresAt: Math.floor(Date.now() / 1000) + 86400,
    },
  })
);

// DynamoDB Stream → Lambda
export const handler = async (event: DynamoDBStreamEvent) => {
  for (const record of event.Records) {
    if (record.eventName === "INSERT") {
      const item = unmarshall(record.dynamodb!.NewImage!);
      await es.index({ index: "tasks", id: item.taskId, body: item });
    }
  }
};

Key design: PK quyết định partition, SK cho range query

Mình thấy key design là skill quan trọng nhất khi làm việc với DynamoDB. On-Demand hay Provisioned chỉ là chuyện nhỏ sau khi đã thiết kế đúng access pattern.

  • On-Demand → Provisioned khi access pattern ổn định
  • GSI: query theo key khác, eventual consistency, thêm cost
  • Single-table: PK/SK prefix phân biệt entity — Bài 41 deep dive
  • TTL: auto expire cho session, cache, token
  • Stream → Lambda: search index, notification, cache invalidation

Bài sau: Phần 14: Route 53 — domain, DNS, health check, routing policy