Claude Code und AWS DynamoDB: Tabellendesign, sichere Writes und Kosten
Praxisleitfaden für Claude Code mit DynamoDB: Keys, lokale Tests, TTL, IAM, Kosten und Hot Partitions.
Claude Code einfach nur zu bitten, “DynamoDB einzubauen”, ist für produktive Software zu ungenau. DynamoDB wirkt schemalos, aber das eigentliche Schema steckt in den Zugriffsmustern: welche Ansicht welche Daten liest, mit welchem Key abgefragt wird, welche Writes nicht überschreiben dürfen und ob sich Traffic gleichmäßig über Partition Keys verteilt.
Dieser Leitfaden nutzt Claude Code als Design-Reviewer für DynamoDB. Wir behandeln Tabellendesign, Partition Keys, einfaches Design gegenüber Single-Table Design, Conditional Writes, TTL, lokale Tests, IAM, Kosten und typische Hot-Partition-Fehler. Ergänzend passen der AWS Lambda Guide, der AWS IAM Guide und der CloudWatch Guide.
Prüfe Details immer in den offiziellen AWS-Dokumenten: data modeling foundations, partition key best practices, Query key condition expressions, condition expressions, TTL, DynamoDB local, throughput capacity und IAM fine-grained access control.
Zugriffsmuster zuerst
Ein Zugriffsmuster ist eine konkrete Operation: Aufgaben eines Projekts listen, eine Aufgabe abschließen, eine Session nach sieben Tagen auslaufen lassen oder ein Webhook-Event nur einmal verarbeiten. Ohne diese Liste erzeugt Claude Code oft generisches CRUD und repariert später mit Scan oder zusätzlichen Indizes.
Nutze vor der Implementierung diesen Prompt:
Bitte prüfe dieses DynamoDB-Design, bevor du Code schreibst.
Anforderungen:
- Aufgaben nach Projekt listen
- Eine Aufgabe per taskId aktualisieren
- Benutzer-Sessions nach 7 Tagen ablaufen lassen
- Jedes webhook eventId nur einmal verarbeiten
Ausgabe:
1. Tabelle der Zugriffsmuster
2. Vorschlag für PK/SK
3. Operationen, die Query nutzen können, und solche, die es nicht können
4. Notwendige Conditional Writes
5. Risiken für Hot Partitions und Kosten
Query in DynamoDB braucht eine Gleichheitsbedingung auf dem Partition Key. Der Sort Key kann danach weiter einschränken, etwa mit begins_with. Eine FilterExpression ersetzt kein SQL-WHERE, das Read-Kosten spart; sie filtert nach dem Lesen. Wenn Claude Code für eine Listenansicht Scan vorschlägt, ist das zunächst ein Designproblem.
Einfach oder Single Table
Single-Table Design speichert mehrere Entitätstypen in einer Tabelle, meist mit generischen Keys wie PK und SK. Das ist stark, wenn eine Oberfläche zusammenhängende Daten in einem Query braucht. Es erhöht aber auch die Anforderungen an Key-Namen, IAM, Streams und Backups.
Ein einfaches Design trennt Entitäten stärker oder hält eine Tabelle auf wenige verwandte Muster begrenzt. Für MVPs, interne Tools und Teams am Anfang mit Claude Code ist das oft besser prüfbar.
| Kriterium | Einfaches Design | Single-Table Design |
|---|---|---|
| Start | Leicht erklärbar | Strengere Review nötig |
| Mehrere Entitäten | Mehrere Reads möglich | Oft ein Query |
| IAM | Trennung nach Tabelle | LeadingKeys sind zentral |
| Änderungen | Neue Tabelle ist einfach | Key-Namen müssen stabil bleiben |
| Gut für | MVP, Lernen, kleine Tools | Stabile Fachdomäne |
Das Beispiel nutzt eine Tabelle, aber nur für Projekte, Aufgaben, Sessions und Webhook-Deduplizierung.
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
Queries:
- Projektaufgaben: PK = PROJECT#alpha AND begins_with(SK, TASK#)
- Benutzer-Sessions: PK = USER#u-001 AND begins_with(SK, SESSION#)
- Webhook-Dedupe: Conditional PutItem auf dieselbe PK/SK
Konkrete Use Cases
Erstens: ein Aufgabenboard pro Projekt. Mit PK = PROJECT#projectId und SK = TASK#taskId wird die Liste ein Query. Falls Statusfilter nötig sind, sollte Claude Code begründen, ob ein GSI wirklich gebraucht wird oder ob eine Projektsicht mit Sortierung in der Anwendung reicht.
Zweitens: Sessions, Einladungen und temporäre Tokens. TTL nutzt eine Unix-Epoch-Zeit in Sekunden als Number. Das hilft beim Aufräumen temporärer Daten, ist aber kein exakter Scheduler. Abgelaufene Items können bis zur Hintergrundlöschung noch lesbar sein. Prüfe expiresAt daher auch in der Anwendung.
Drittens: Idempotenz für Webhooks. Provider senden Events erneut. Mit WEBHOOK#provider, EVENT#eventId und attribute_not_exists(PK) AND attribute_not_exists(SK) gewinnt nur die erste Verarbeitung.
Viertens: einfaches Rate Limiting. PK = RATE#userId und SK = WINDOW#2026-06-03T10:00 funktioniert für kleinere interne APIs. Bei hoher Last kann ein einzelner Nutzer oder Tenant aber eine Hot Partition erzeugen.
Lokales Setup
Starte 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
Lege die lokale Tabelle an:
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
Aktiviere das TTL-Attribut:
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
Installiere die SDK-Pakete:
npm init -y
npm install @aws-sdk/client-dynamodb @aws-sdk/lib-dynamodb
Kopierbare Implementierung
Speichere den folgenden Code als app.mjs. Er erstellt eine Aufgabe, listet Projektaufgaben, schließt eine Aufgabe per Conditional Update ab und erstellt eine Session mit 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
Refactor-Prompt:
Refactore app.mjs in einen Lambda Handler.
Entferne ConditionExpression nicht.
Füge keinen Scan hinzu, außer du erklärst, warum Query nicht funktioniert.
Belasse expiresAt als Number in Unix-Epoch-Sekunden.
Behalte ReturnConsumedCapacity in der Entwicklungsumgebung.
IAM, Kosten und Fehler
Bei einer gemeinsamen Tabelle ist Zugriff auf die ganze Tabelle oft zu breit. Mit dynamodb:LeadingKeys kann IAM auf Partition Keys begrenzt werden.
{
"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 ist bequem zum Start, Provisioned kann bei stabiler Last günstiger planbar sein. Typische Fehler sind Scan für Listen, FilterExpression als SQL-Ersatz, feste Keys wie GLOBAL, falsche TTL-Erwartungen, GSIs ohne Zugriffsmuster, fehlende Conditional Writes und ein lokaler Endpoint in Produktion.
Prüfe diese DynamoDB-Implementierung auf Scan-Abhängigkeit, Hot Partitions, TTL-Missverständnisse, fehlende Conditional Writes, zu breite IAM-Rechte und On-Demand-Kostenspitzen. Gib zuerst Findings aus, dann Korrekturen.
Wiederverwendbare Prompts und Checklisten findest du in den ClaudeCodeLab Produkten. Für Teams mit AWS, IAM, CI-Reviews und Produktionsnachweisen ist Claude Code Training und Beratung der nächste Schritt.
Fazit
DynamoDB wird zuverlässig, wenn Zugriffsmuster, Keys, Fehlerbedingungen, TTL, IAM und Kostensignale explizit sind. Claude Code beschleunigt die Umsetzung, aber die Grenzen müssen vorher klar sein.
Praxisnotiz (実際に試した結果): Am besten funktionierte die Reihenfolge Zugriffsmuster zuerst, dann PK/SK und Fehlerfälle prüfen, dann Conditional Writes lokal ausführen. Besonders attribute_not_exists für Webhook-Deduplizierung ist eine kleine, aber sehr wirksame Review-Regel.
Kostenloses PDF: Claude-Code-Cheatsheet
E-Mail eintragen und eine Seite mit Befehlen, Review-Gewohnheiten und sicheren Workflows herunterladen.
Wir schützen Ihre Daten und senden keinen Spam.
Über den Autor
Masa
Engineer für praktische Claude-Code-Workflows und Team-Einführung.
Ähnliche Artikel
Claude Code Workflow von Obsidian zu CLAUDE.md
Obsidian-Arbeitsnotizen in CLAUDE.md-Betriebsnotizen verwandeln und Kontext nicht ständig neu erklären.
Claude Code Revenue CTA Routing: Artikel zu PDF, Gumroad und Beratung führen
Ein Claude-Code-Ablauf, der Leser nach Absicht zu Gratis-PDF, Gumroad oder Beratung führt.
Claude-Code-Team-Handoff-Regeln: Belege, Berechtigungen, Rollback und Umsatzpfade
Ein praktisches Claude-Code-Handoff für Review-Belege, Berechtigungen, Rollback, Gratis-PDF, Gumroad und Beratung.