Claude Code and AWS DynamoDB: Practical Table Design, Writes, TTL, and Cost
A practical Claude Code and DynamoDB guide covering keys, local tests, TTL, IAM, costs, and hot partitions.
Asking Claude Code to “add DynamoDB” is too vague for production work. DynamoDB looks flexible because items do not need a fixed relational schema, but the real schema is your access pattern: which item you read, which key you query, what must not be overwritten, and how traffic spreads across partition keys.
This guide shows a practical workflow for using Claude Code with AWS DynamoDB without turning the database into a pile of Scan calls. We will cover table design, partition keys, simple design versus single-table design, conditional writes, TTL, local testing, IAM boundaries, cost, and hot partition mistakes. For adjacent ClaudeCodeLab material, pair it with the AWS Lambda guide, AWS IAM guide, and AWS CloudWatch guide.
Keep the official AWS documentation open while implementing: data modeling foundations, partition key best practices, Query key condition expressions, condition expressions, TTL, DynamoDB local, throughput capacity, and IAM fine-grained access control.
Start With Access Patterns
An access pattern is the exact read or write your application performs: list tasks for a project, complete one task, expire a session after seven days, or process a webhook only once. If Claude Code does not know those operations, it will usually produce generic CRUD code and add indexes later as a patch.
Use a prompt like this before asking for code:
Review this DynamoDB design before writing code.
Requirements:
- List tasks by project
- Update one task by taskId
- Expire user sessions after 7 days
- Process each webhook eventId only once
Return:
1. Access pattern table
2. PK/SK proposal
3. Operations that can use Query and operations that cannot
4. Conditional writes needed for safety
5. Hot partition and cost risks
This framing matters because DynamoDB Query requires an equality condition on the partition key, with optional sort key conditions such as begins_with. A filter expression is not a substitute for a good key. It filters after the read has already consumed capacity. When Claude Code proposes Scan for a user-facing list page, treat that as a design bug until proven otherwise.
Simple Design or Single Table
Single-table design stores multiple entity types in one table, usually with generic keys such as PK and SK. It can reduce round trips when one screen needs related entities together. It can also make the learning curve, IAM policy, stream processing, and backup boundary more demanding.
Simple design keeps entities in separate tables, or at least keeps a single table focused on a small set of related access patterns. For many MVPs and internal tools, that is easier to review and safer to hand to Claude Code.
| Decision | Simple design | Single-table design |
|---|---|---|
| Early development | Easier to explain | Needs stronger review |
| Multi-entity screens | Multiple reads may be needed | Often one Query |
| IAM | Table-level separation is easier | Leading key rules matter |
| Change tolerance | Add a table when needed | Key naming must stay consistent |
| Best fit | Small apps and learning | Stable business access patterns |
The example below uses one table but keeps the scope small: projects, tasks, sessions, and webhook idempotency. It is a practical middle ground for learning.
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:
- Project task list: PK = PROJECT#alpha AND begins_with(SK, TASK#)
- User sessions: PK = USER#u-001 AND begins_with(SK, SESSION#)
- Webhook dedupe: conditional PutItem on the same PK/SK
Concrete Use Cases
The first use case is a project task board. Put all tasks for one project under PK = PROJECT#projectId, and use SK = TASK#taskId. The list screen becomes a Query, not a Scan, and a single task update uses the full key.
The second use case is session and invite expiry. DynamoDB TTL uses a per-item numeric Unix epoch timestamp in seconds. It is useful for sessions, password reset links, temporary imports, and cached API responses. Do not treat TTL as a real-time scheduler: expired items can remain readable until DynamoDB deletes them in the background, so your application should still check the timestamp when the data is security-sensitive.
The third use case is webhook idempotency. Idempotency means repeated delivery does not repeat the side effect. Payment providers and SaaS webhooks often retry events. A conditional PutItem with attribute_not_exists(PK) AND attribute_not_exists(SK) gives you a simple “first writer wins” guard.
A fourth use case is rate limiting. You can store counters by time window, such as PK = RATE#userId and SK = WINDOW#2026-06-03T10:00. This is useful for modest internal APIs, but high-traffic public APIs need extra care because one popular user, tenant, or route can create a hot partition.
Local Runnable Setup
Create a local DynamoDB service first. This Compose file follows the same shape as the AWS documentation’s Docker example.
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
Create the table locally. Check the --endpoint-url before running the command so you do not create a real table by accident.
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
Enable a TTL attribute. Local testing is good for checking the attribute shape and application logic; do not use it to prove production deletion timing.
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
Install the AWS SDK for JavaScript.
npm init -y
npm install @aws-sdk/client-dynamodb @aws-sdk/lib-dynamodb
Copy-Paste Implementation
Save this as app.mjs. It creates a task, lists project tasks, completes one task with a conditional update, and creates a TTL-backed session item.
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
When you ask Claude Code to refactor this into Lambda or an API route, protect the important behavior:
Refactor app.mjs into a Lambda handler.
Do not remove ConditionExpression.
Do not add Scan unless you explain why Query cannot work.
Keep expiresAt as a Unix epoch seconds Number.
Keep ReturnConsumedCapacity in development.
IAM and Safety Boundaries
For a single table, table-level access is often too broad. DynamoDB IAM condition keys can restrict access by leading partition key. The following example assumes a principal tag called projectId; replace the account, region, table name, and tagging model for your environment.
{
"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}"
]
}
}
}
]
}
Review generated policies carefully. Ask why Scan is needed, whether index ARNs are included, whether production and development tables are separated, and whether ReturnValues or projection rules can expose attributes you did not intend to return.
Cost and Hot Partition Mistakes
On-demand mode is usually the easiest starting point because you pay per request and do not need to forecast read and write capacity on day one. Provisioned mode can still be better for predictable traffic where you want tighter cost control. Claude Code cannot infer the right mode from “make it cheap”; provide item size, expected read/write count, and peak shape.
The most common DynamoDB mistakes are practical, not theoretical:
- Building list pages with
Scan - Treating
FilterExpressionlike a SQLWHEREclause - Using keys such as
GLOBAL,TODAY, orDEFAULTfor high-traffic items - Assuming TTL deletes items at the exact expiration second
- Adding GSIs without naming the access pattern they serve
- Skipping conditional writes for orders, webhooks, and state transitions
- Leaving the local endpoint in production configuration
A useful review prompt is:
Audit this DynamoDB implementation for Scan dependency, hot partition risk, TTL misunderstanding, missing conditional writes, excessive IAM permissions, and on-demand cost spikes. Return findings first, then suggested fixes.
For repeatable prompts, checklists, and review templates, use the ClaudeCodeLab products. For team rollout around AWS, IAM, review gates, and production verification, use Claude Code training and consultation.
Final Notes
DynamoDB rewards explicit design. Decide the access patterns, partition keys, sort key prefixes, conditional writes, TTL behavior, IAM limits, and cost signals before generating framework code. Claude Code is useful because it can turn those constraints into implementation quickly, but it should not be allowed to invent the constraints.
Hands-on note: the best workflow was to ask Claude Code for an access-pattern table first, review PK/SK and failure cases second, and only then run conditional writes against DynamoDB Local. The webhook dedupe example with attribute_not_exists is especially valuable because it turns an abstract NoSQL lesson into a testable production safety guard.
Free PDF: Claude Code Cheatsheet
Enter your email and download the one-page Claude Code cheatsheet for commands, review habits, and safe workflows.
We handle your data with care and never send spam.
Level up your Claude Code workflow
Start with the free PDF, use Gumroad guides when you need repeatable workflows, and book consultation when rollout or revenue paths need human judgment.
About the Author
Masa
Engineer focused on practical Claude Code workflows. Runs claudecode-lab.com, a 10-language technical media site.
Related Posts
Claude Code Free PDF Funnel Checklist: Turn Article Traffic into Signups and Product Clicks
A Claude Code checklist for routing article readers to the free PDF, Gumroad products, and consultation.
Claude Code Obsidian to CLAUDE.md Workflow: Stop Re-explaining Context
Turn Obsidian working notes into concise CLAUDE.md operating notes that make Claude Code sessions easier to resume.
Claude Code Revenue CTA Routing: Send Articles to PDF, Gumroad, and Consultation
A Claude Code workflow for routing article readers to the free PDF, Gumroad products, or consultation by intent.
Related Products
50 Battle-Tested Claude Code Prompt Templates
Copy, paste, ship. 50 production-ready prompts.
Use proven prompts for code review, refactoring, testing, documentation, debugging, architecture, and incident response.
The Complete Claude Code Setup & Configuration Guide
From install to team-ready workflow.
A practical guide to installation, CLAUDE.md, hooks, MCP servers, permissions, IDE setup, and CI/CD workflows.