Claude Code y AWS DynamoDB: diseño de tablas, escrituras seguras y costes
Guía práctica de Claude Code con DynamoDB: claves, pruebas locales, TTL, IAM, costes y particiones calientes.
Pedirle a Claude Code “añade DynamoDB” no basta para una aplicación real. DynamoDB parece flexible porque cada item puede tener atributos distintos, pero el verdadero diseño está en los patrones de acceso: qué lee la pantalla, con qué clave se consulta, qué escritura no debe sobrescribir datos y cómo se reparte el tráfico entre particiones.
Esta guía usa Claude Code como asistente de diseño, no solo como generador de código. Veremos claves de partición, diseño simple frente a single-table design, escrituras condicionales, TTL, pruebas locales, IAM, costes y errores de hot partition. Para completar el contexto en ClaudeCodeLab, revisa también la guía de AWS Lambda, la guía de AWS IAM y la guía de CloudWatch.
Trabaja con la documentación oficial de AWS abierta: data modeling foundations, partition key best practices, Query key condition expressions, condition expressions, TTL, DynamoDB local, throughput capacity y IAM fine-grained access control.
Empieza por los patrones de acceso
Un patrón de acceso es una operación concreta de la aplicación: listar tareas por proyecto, completar una tarea, caducar una sesión en siete días o procesar un webhook una sola vez. Si Claude Code no conoce estas operaciones, suele producir CRUD genérico y compensarlo con índices o Scan.
Antes de pedir implementación, usa un prompt como este:
Revisa este diseño DynamoDB antes de escribir código.
Requisitos:
- Listar tareas por proyecto
- Actualizar una tarea por taskId
- Caducar sesiones de usuario después de 7 días
- Procesar cada webhook eventId solo una vez
Devuelve:
1. Tabla de patrones de acceso
2. Propuesta de PK/SK
3. Operaciones que pueden usar Query y las que no
4. Escrituras condicionales necesarias
5. Riesgos de partición caliente y coste
La razón es importante: Query en DynamoDB exige una condición de igualdad sobre la partition key y permite acotar con la sort key. FilterExpression no es un WHERE de SQL que ahorre lectura; filtra después de consumir capacidad. Si Claude Code propone Scan para una lista de usuario, detén la implementación y revisa el diseño.
Diseño simple o single-table
El single-table design guarda varios tipos de entidad en una tabla con claves genéricas como PK y SK. Puede reducir lecturas cuando una pantalla necesita datos relacionados, pero complica el aprendizaje, IAM, Streams, backups y nombres de clave.
El diseño simple separa entidades por tabla o limita una tabla a pocos patrones relacionados. Para MVP, herramientas internas y equipos que empiezan con Claude Code, suele ser más fácil de revisar.
| Criterio | Diseño simple | Single-table design |
|---|---|---|
| Inicio | Fácil de explicar | Requiere revisión fuerte |
| Pantallas con varias entidades | Puede requerir varias lecturas | A menudo basta un Query |
| IAM | Separación por tabla | Importan las LeadingKeys |
| Cambios | Puedes añadir tablas | Las claves deben ser consistentes |
| Mejor uso | MVP y aprendizaje | Negocio con patrones estables |
El ejemplo usa una tabla, pero con alcance limitado: proyectos, tareas, sesiones y deduplicación 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:
- Tareas del proyecto: PK = PROJECT#alpha AND begins_with(SK, TASK#)
- Sesiones del usuario: PK = USER#u-001 AND begins_with(SK, SESSION#)
- Dedupe de webhooks: PutItem condicional sobre la misma PK/SK
Casos prácticos
Primer caso: un tablero de tareas por proyecto. Con PK = PROJECT#projectId y SK = TASK#taskId, la pantalla de lista se resuelve con Query. Si además necesitas filtrar por estado, pide a Claude Code que justifique si hace falta un GSI o si basta con consultar el proyecto y ordenar en la aplicación.
Segundo caso: sesiones, invitaciones y tokens temporales. TTL usa un timestamp Unix epoch en segundos, guardado como Number. Sirve para borrar datos que ya no importan, pero no es un programador exacto. Un item caducado puede seguir leyéndose hasta que DynamoDB lo borre en segundo plano, así que la aplicación debe comprobar expiresAt cuando haya seguridad de por medio.
Tercer caso: idempotencia de webhooks. La idempotencia evita repetir el efecto secundario cuando un proveedor reenvía un evento. Usa WEBHOOK#provider y EVENT#eventId como clave, y attribute_not_exists(PK) AND attribute_not_exists(SK) para que solo gane el primer procesamiento.
Cuarto caso: rate limiting. Puedes usar PK = RATE#userId y SK = WINDOW#2026-06-03T10:00. Funciona en APIs internas moderadas, pero en tráfico alto puede crear una partición caliente si un usuario, tenant o ruta concentra demasiadas escrituras.
Entorno local ejecutable
Arranca DynamoDB Local con Docker Compose:
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
Crea la tabla local. Revisa el --endpoint-url antes de ejecutar.
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
Activa el atributo 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
Instala dependencias:
npm init -y
npm install @aws-sdk/client-dynamodb @aws-sdk/lib-dynamodb
Implementación para copiar y ejecutar
Guarda esto como app.mjs. Crea una tarea, lista tareas del proyecto, completa una tarea con condición y crea una sesión con 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
Al pedir a Claude Code que lo convierta en Lambda, protege las reglas importantes:
Refactoriza app.mjs como Lambda handler.
No elimines ConditionExpression.
No añadas Scan salvo que expliques por qué Query no sirve.
Mantén expiresAt como Number en Unix epoch seconds.
Mantén ReturnConsumedCapacity en desarrollo.
IAM, costes y riesgos
En una tabla compartida, permitir toda la tabla suele ser demasiado amplio. Con dynamodb:LeadingKeys puedes limitar el acceso por partition key. Sustituye cuenta, región, tabla y modelo de tags según tu entorno.
{
"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 es cómodo para empezar porque pagas por petición. Provisioned puede convenir si el tráfico es estable y quieres previsibilidad. Los errores más comunes son usar Scan para listas, confiar en FilterExpression, concentrar tráfico en claves como GLOBAL, creer que TTL borra al segundo exacto, añadir GSI sin patrón de acceso y omitir escrituras condicionales en pedidos o webhooks.
Audita esta implementación DynamoDB por dependencia de Scan, particiones calientes, malentendidos de TTL, escrituras condicionales faltantes, permisos IAM excesivos y picos de coste on-demand. Devuelve hallazgos primero y luego correcciones.
Para convertir estos prompts y checklists en material reutilizable, visita productos de ClaudeCodeLab. Para equipos que quieren revisar AWS, IAM, CI y validación de producción, la ruta natural es formación y consultoría de Claude Code.
Cierre
DynamoDB funciona bien cuando el diseño es explícito: patrones de acceso, claves, condiciones de fallo, TTL, IAM y señales de coste. Claude Code acelera la implementación, pero no debe inventar esas restricciones sin revisión.
Nota práctica (実際に試した結果): el flujo más fiable fue pedir primero una tabla de patrones de acceso, revisar después PK/SK y casos de fallo, y solo entonces ejecutar escrituras condicionales contra DynamoDB Local. El ejemplo de webhook con attribute_not_exists convierte una idea abstracta en una comprobación de seguridad concreta.
PDF gratis: cheatsheet de Claude Code
Introduce tu email y descarga una hoja con comandos, hábitos de revisión y flujos seguros.
Cuidamos tus datos y no enviamos spam.
Sobre el autor
Masa
Ingeniero enfocado en workflows prácticos con Claude Code.
Artículos relacionados
Workflow de Obsidian a CLAUDE.md con Claude Code
Convierte notas de trabajo de Obsidian en notas operativas de CLAUDE.md para no repetir contexto.
Claude Code Revenue CTA Routing: de artículos a PDF, Gumroad y consulta
Un flujo con Claude Code para dirigir lectores a PDF gratis, Gumroad o consulta según intención.
Reglas de handoff para equipos con Claude Code: evidencia, permisos, rollback e ingresos
Formato práctico para entregar trabajo de Claude Code con pruebas, permisos, rollback, PDF gratis, Gumroad y consulta.