Use Cases (Atualizado: 03/06/2026)

Claude Code e AWS DynamoDB: design de tabela, escritas seguras e custos

Guia prático de Claude Code com DynamoDB: chaves, testes locais, TTL, IAM, custos e hot partitions.

Claude Code e AWS DynamoDB: design de tabela, escritas seguras e custos

Pedir ao Claude Code “adicione DynamoDB” é pouco para produção. DynamoDB parece sem schema, mas o schema real está nos padrões de acesso: qual tela lê quais dados, qual chave será consultada, quais escritas não podem sobrescrever registros e como o tráfego se distribui entre partition keys.

Este guia usa Claude Code como parceiro de revisão de design. Vamos passar por design de tabela, partition keys, design simples versus single-table design, escritas condicionais, TTL, testes locais, IAM, custos e erros de hot partition. Para conectar com outras partes da stack, veja também o guia de AWS Lambda, o guia de AWS IAM e o guia de CloudWatch.

Confira sempre a documentação oficial da AWS: data modeling foundations, partition key best practices, Query key condition expressions, condition expressions, TTL, DynamoDB local, throughput capacity e IAM fine-grained access control.

Comece pelos padrões de acesso

Um padrão de acesso é uma operação concreta: listar tarefas de um projeto, concluir uma tarefa, expirar uma sessão em sete dias ou processar um webhook apenas uma vez. Sem isso, Claude Code tende a gerar CRUD genérico e depois compensar com Scan ou índices.

Antes de pedir código, use este prompt:

Revise este design DynamoDB antes de escrever código.
Requisitos:
- Listar tarefas por projeto
- Atualizar uma tarefa por taskId
- Expirar sessões de usuário após 7 dias
- Processar cada webhook eventId apenas uma vez

Retorne:
1. Tabela de padrões de acesso
2. Proposta de PK/SK
3. Operações que podem usar Query e as que não podem
4. Escritas condicionais necessárias
5. Riscos de hot partition e custo

Em DynamoDB, Query precisa de igualdade na partition key e pode restringir pela sort key. FilterExpression não economiza leitura como um WHERE SQL; ele filtra depois que a capacidade já foi consumida. Se Claude Code sugerir Scan para uma listagem de usuário, trate como problema de design.

Design simples ou single-table

Single-table design guarda vários tipos de entidade em uma tabela com chaves genéricas como PK e SK. Ele pode reduzir round trips quando uma tela precisa de dados relacionados, mas aumenta a complexidade de IAM, Streams, backups e evolução de chaves.

Design simples separa entidades ou limita a tabela a poucos padrões. Para MVPs, ferramentas internas e times começando com Claude Code, costuma ser mais fácil de revisar.

CritérioDesign simplesSingle-table design
InícioFácil de explicarRequer revisão forte
Tela com várias entidadesPode precisar de múltiplas leiturasMuitas vezes um Query
IAMSeparação por tabelaLeadingKeys importam
MudançasNova tabela é simplesNomes de chave precisam ser estáveis
Melhor usoMVP e aprendizadoDomínio com padrões estáveis

O exemplo usa uma tabela, mas com escopo pequeno: projetos, tarefas, sessões e deduplicação de webhooks.

ClaudeCodeLabDemo

PK                 SK                   entityType
PROJECT#alpha      META                 Project
PROJECT#alpha      TASK#task-001        Task
USER#u-001         SESSION#s-001        Session
WEBHOOK#stripe     EVENT#evt_001        WebhookEvent

Consultas:
- Tarefas do projeto: PK = PROJECT#alpha AND begins_with(SK, TASK#)
- Sessões do usuário: PK = USER#u-001 AND begins_with(SK, SESSION#)
- Dedupe de webhook: PutItem condicional na mesma PK/SK

Casos de uso

Primeiro: quadro de tarefas por projeto. Com PK = PROJECT#projectId e SK = TASK#taskId, a lista vira um Query. Se precisar filtrar por status, peça ao Claude Code para justificar se um GSI é necessário ou se uma leitura por projeto já resolve.

Segundo: sessões, convites e tokens temporários. TTL usa um timestamp Unix epoch em segundos, salvo como Number. Ele ajuda a limpar dados temporários, mas não é um agendador exato. Um item expirado pode continuar legível até a exclusão em segundo plano, então a aplicação também deve checar expiresAt.

Terceiro: idempotência de webhooks. Provedores reenviam eventos. Com WEBHOOK#provider, EVENT#eventId e attribute_not_exists(PK) AND attribute_not_exists(SK), apenas o primeiro processamento vence.

Quarto: rate limit simples. PK = RATE#userId e SK = WINDOW#2026-06-03T10:00 funcionam em APIs internas moderadas, mas podem virar hot partition quando um usuário ou tenant concentra tráfego.

Setup local

Suba o DynamoDB Local:

services:
  dynamodb-local:
    image: "amazon/dynamodb-local:latest"
    command: "-jar DynamoDBLocal.jar -sharedDb -dbPath ./data"
    ports:
      - "8000:8000"
    volumes:
      - "./docker/dynamodb:/home/dynamodblocal/data"
    working_dir: /home/dynamodblocal
docker compose up -d
export AWS_ACCESS_KEY_ID=fakeMyKeyId
export AWS_SECRET_ACCESS_KEY=fakeSecretAccessKey
export AWS_REGION=us-west-2

Crie a tabela local:

aws dynamodb create-table \
  --table-name ClaudeCodeLabDemo \
  --attribute-definitions AttributeName=PK,AttributeType=S AttributeName=SK,AttributeType=S \
  --key-schema AttributeName=PK,KeyType=HASH AttributeName=SK,KeyType=RANGE \
  --billing-mode PAY_PER_REQUEST \
  --endpoint-url http://localhost:8000 \
  --region us-west-2

Ative TTL:

aws dynamodb update-time-to-live \
  --table-name ClaudeCodeLabDemo \
  --time-to-live-specification "Enabled=true,AttributeName=expiresAt" \
  --endpoint-url http://localhost:8000 \
  --region us-west-2

Instale dependências:

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

Implementação copiável

Salve como app.mjs. O script cria uma tarefa, lista tarefas do projeto, conclui uma tarefa com condição e cria uma sessão com TTL.

import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import {
  DynamoDBDocumentClient,
  PutCommand,
  QueryCommand,
  UpdateCommand,
} from "@aws-sdk/lib-dynamodb";

const TABLE_NAME = process.env.TABLE_NAME ?? "ClaudeCodeLabDemo";
const isLocal = process.env.DDB_LOCAL !== "0";

const client = new DynamoDBClient({
  region: process.env.AWS_REGION ?? "us-west-2",
  ...(isLocal
    ? {
        endpoint: "http://localhost:8000",
        credentials: {
          accessKeyId: "fakeMyKeyId",
          secretAccessKey: "fakeSecretAccessKey",
        },
      }
    : {}),
});

const ddb = DynamoDBDocumentClient.from(client, {
  marshallOptions: { removeUndefinedValues: true },
});

const nowIso = () => new Date().toISOString();
const ttlAfterDays = (days) => Math.floor(Date.now() / 1000) + days * 86400;
const taskKey = (projectId, taskId) => ({
  PK: `PROJECT#${projectId}`,
  SK: `TASK#${taskId}`,
});

async function createTask({ projectId, taskId, title, ownerId }) {
  const item = {
    ...taskKey(projectId, taskId),
    entityType: "Task",
    title,
    ownerId,
    status: "OPEN",
    createdAt: nowIso(),
    updatedAt: nowIso(),
  };

  await ddb.send(
    new PutCommand({
      TableName: TABLE_NAME,
      Item: item,
      ConditionExpression: "attribute_not_exists(PK) AND attribute_not_exists(SK)",
    }),
  );

  return item;
}

async function listProjectTasks(projectId) {
  const result = await ddb.send(
    new QueryCommand({
      TableName: TABLE_NAME,
      KeyConditionExpression: "PK = :pk AND begins_with(SK, :taskPrefix)",
      ExpressionAttributeValues: {
        ":pk": `PROJECT#${projectId}`,
        ":taskPrefix": "TASK#",
      },
      ReturnConsumedCapacity: "TOTAL",
    }),
  );

  console.log("consumed capacity:", result.ConsumedCapacity);
  return result.Items ?? [];
}

async function completeTask({ projectId, taskId, expectedOwnerId }) {
  const result = await ddb.send(
    new UpdateCommand({
      TableName: TABLE_NAME,
      Key: taskKey(projectId, taskId),
      UpdateExpression: "SET #status = :done, updatedAt = :now",
      ConditionExpression: "ownerId = :ownerId AND #status <> :done",
      ExpressionAttributeNames: {
        "#status": "status",
      },
      ExpressionAttributeValues: {
        ":done": "DONE",
        ":ownerId": expectedOwnerId,
        ":now": nowIso(),
      },
      ReturnValues: "ALL_NEW",
    }),
  );

  return result.Attributes;
}

async function createSession({ userId, sessionId }) {
  await ddb.send(
    new PutCommand({
      TableName: TABLE_NAME,
      Item: {
        PK: `USER#${userId}`,
        SK: `SESSION#${sessionId}`,
        entityType: "Session",
        createdAt: nowIso(),
        expiresAt: ttlAfterDays(7),
      },
      ConditionExpression: "attribute_not_exists(PK) AND attribute_not_exists(SK)",
    }),
  );
}

async function main() {
  const projectId = "alpha";
  const taskId = `task-${Date.now()}`;

  await createTask({
    projectId,
    taskId,
    title: "Review DynamoDB key design",
    ownerId: "masa",
  });

  await createSession({
    userId: "masa",
    sessionId: `session-${Date.now()}`,
  });

  console.log(await listProjectTasks(projectId));
  console.log(
    await completeTask({
      projectId,
      taskId,
      expectedOwnerId: "masa",
    }),
  );
}

main().catch((error) => {
  if (error.name === "ConditionalCheckFailedException") {
    console.error("Condition failed:", error.message);
    process.exit(2);
  }

  console.error(error);
  process.exit(1);
});
DDB_LOCAL=1 node app.mjs

Prompt para refatorar:

Refatore app.mjs para um Lambda handler.
Não remova ConditionExpression.
Não adicione Scan sem explicar por que Query não serve.
Mantenha expiresAt como Number em Unix epoch seconds.
Mantenha ReturnConsumedCapacity em desenvolvimento.

IAM, custo e armadilhas

Em uma tabela compartilhada, acesso à tabela inteira costuma ser amplo demais. dynamodb:LeadingKeys permite restringir pela partition key.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "ProjectScopedDynamoDBAccess",
      "Effect": "Allow",
      "Action": [
        "dynamodb:GetItem",
        "dynamodb:PutItem",
        "dynamodb:UpdateItem",
        "dynamodb:DeleteItem",
        "dynamodb:Query"
      ],
      "Resource": [
        "arn:aws:dynamodb:ap-northeast-1:123456789012:table/ClaudeCodeLabProd",
        "arn:aws:dynamodb:ap-northeast-1:123456789012:table/ClaudeCodeLabProd/index/*"
      ],
      "Condition": {
        "ForAllValues:StringEquals": {
          "dynamodb:LeadingKeys": [
            "PROJECT#${aws:PrincipalTag/projectId}"
          ]
        }
      }
    }
  ]
}

On-demand é ótimo para começar, enquanto provisioned pode fazer sentido para tráfego previsível. Erros comuns: listas com Scan, FilterExpression usado como SQL, chaves fixas como GLOBAL, TTL tratado como exclusão imediata, GSI sem padrão de acesso, falta de escrita condicional e endpoint local em produção.

Audite esta implementação DynamoDB para dependência de Scan, hot partition, mal-entendido de TTL, falta de escritas condicionais, IAM amplo demais e picos de custo on-demand. Liste achados primeiro e depois correções.

Para prompts e checklists reutilizáveis, veja os produtos ClaudeCodeLab. Para padronizar AWS, IAM, CI e validação em equipe, use treinamento e consultoria Claude Code.

Fechamento

DynamoDB funciona melhor quando padrões de acesso, chaves, condições de falha, TTL, IAM e sinais de custo ficam explícitos. Claude Code acelera a implementação, mas não deve inventar essas restrições sem revisão.

Nota prática (実際に試した結果): o fluxo mais confiável foi pedir primeiro a tabela de padrões de acesso, revisar PK/SK e falhas depois, e só então executar escritas condicionais no DynamoDB Local. O exemplo de webhook com attribute_not_exists vira uma regra de revisão simples e valiosa.

#claude-code #aws #dynamodb #nosql #typescript #database
Grátis

PDF grátis: cheatsheet do Claude Code

Informe seu e-mail e baixe uma página com comandos, hábitos de revisão e workflows seguros.

Cuidamos dos seus dados e não enviamos spam.

Masa

Sobre o autor

Masa

Engenheiro focado em workflows práticos com Claude Code.