Skills DirectorySkills Directory
SkillsLearnSecurityCategoriesDocsCommunityBlog
Sign InSubmit Skill
Skills Directory

Security-tested agent skills for Claude, coding agents, and AI workflows.

Directory

  • Browse Skills
  • All Skills A–Z
  • Claude Skills
  • Claude Code Skills
  • Agent Skills
  • Categories
  • Authors
  • Submit a Skill

Learn

  • Learn Hub
  • Install Claude Skills
  • Write SKILL.md
  • Skills vs MCP
  • Directories Compared

Security

  • Security
  • Methodology
  • Secure Claude Skills
  • Security Badges

Company

  • About
  • Community
  • Blog
  • API Docs
  • Advertise

2026 Skills Directory. All rights reserved.

ProTermsPrivacyRefunds
Back to skills

Dynamodb Patterns

ASecurity

DynamoDB single-table design, composite keys, GSI design, conditional writes, BatchWrite/BatchGet chunking, atomic counters, TTL, streams, and cross-tenant isolation patterns. Auto-fires for files importing from `@aws-sdk/lib-dynamodb` or `@aws-sdk/client-dynamodb`.

12 stars
0 votes
0 copies
0 views
Added 9/28/2026
ai-agentsgosqlexpressawsgitdatabasebackendsecurity

Works with

cli

Security Analysis

A100/100

Scanned 9/28/2026

Install to Claude Code

$npx -y skills add Nmor/the-claude-council --skill dynamodb-patterns --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Dynamodb Patterns?

Add the live security badge to your README — it updates automatically with every re-scan.

Security grade badge for Dynamodb Patterns
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/nmor-dynamodb-patterns/badge)](https://www.skillsdirectory.com/skills/nmor-dynamodb-patterns)

More formats (shields.io, HTML) on the badges page.

Files
SKILL.md
---
name: dynamodb-patterns
description: DynamoDB single-table design, composite keys, GSI design, conditional writes, BatchWrite/BatchGet chunking, atomic counters, TTL, streams, and cross-tenant isolation patterns. Auto-fires for files importing from `@aws-sdk/lib-dynamodb` or `@aws-sdk/client-dynamodb`.
---

# DynamoDB Patterns

> **Size budget: 20 KB** — `token-budget.mjs --check`.

DynamoDB is a key-value store dressed up as a database. Get the access patterns right at design time
and it scales effortlessly; get them wrong and the only fix is a multi-month migration. These
patterns prevent the common production-grade mistakes.

## When to Activate

- Designing a new table or GSI
- Writing or reviewing any `ddb.send(...)` call
- Adding a new query pattern to an existing table
- Auditing for cross-tenant leak risk
- Migrating between key shapes
- Diagnosing throttling / hot-partition incidents

## The Core Rule: Access Patterns Define the Schema

In a relational DB, you model the data, then query it. In DynamoDB, you enumerate the QUERIES first,
then design keys to make each one a single Query (never a Scan). Skipping this step always ends in a
costly redesign.

For each entity, write down:

1. **List X by Y** — needs `PK = Y` or a GSI on Y
2. **Get specific X** — needs the full PK + SK
3. **List X sorted by date** — SK should be a date-prefixed string

If your access pattern is "list all rows that match attribute Z" — that's a Scan, and Scans are
forbidden in production code paths. Either add a GSI on Z or denormalize.

## Tenant Isolation: `organization_id` In Every Key

Multi-tenant systems MUST use `organization_id` (or equivalent) as the partition key on every base
table. Two reasons:

1. **Hot-partition prevention** — one tenant's traffic can't dominate another's RCU/WCU.
2. **Cross-tenant leak prevention** — a forgotten filter on a Query is impossible because the
   Query's PK requires the org.

Allowed deviations:

- Tables keyed by a server-minted unique id (e.g. `connection_id`, `webhook_event_id`) where the org
  is on a GSI.
- Lookup tables (Slack workspace → org, share token → diagram) where the key IS the lookup value.

Both deviations require a defense-in-depth check at read time: confirm the resolved row's
`organization_id` matches the caller's.

A custom ESLint rule (or equivalent static check) should enforce this on every new `QueryCommand` /
`GetCommand` / `UpdateCommand` / `DeleteCommand`.

## Composite Sort Keys For Hierarchies

Sort keys can encode hierarchy:

```text
PK: organization_id        SK: USER#<user_id>
PK: organization_id        SK: TEAM#<team_id>
PK: organization_id        SK: TEAM#<team_id>#MEMBER#<user_id>
```

A single Query with `begins_with(SK, "TEAM#<team_id>")` returns the team plus every member in one
round-trip.

## GSI Design

Each GSI is a separate table in cost. Don't add one casually. Rules:

- **PK = the dimension you query by**, not the original PK. If you query by `(workspace_id)`, the
  GSI PK is `workspace_id`.
- **Project only what callers need** (`KEYS_ONLY` or `INCLUDE`). Default `ALL` doubles your storage
  cost for the table.
- **Sparse indexes** — set the GSI key only on rows that participate in the index (e.g.
  `is_published` only present when `true`). Indexed rows = cost; sparse means cheap.
- **Eventually consistent reads only** — GSIs don't support strong consistency. If you need
  read-your-writes guarantees, query the base table.

## Conditional Writes For Atomicity

DynamoDB has no transactions across partitions, but `ConditionExpression` makes single-item updates
atomic. Use them everywhere a race could land an inconsistent state:

```ts
// "Claim this idempotency key" — fails if already claimed
await ddb.send(new PutCommand({
  TableName: RATE_LIMIT_TABLE,
  Item: { pk: `idemp:${eventId}`, ttl, claimed_at: now },
  ConditionExpression: "attribute_not_exists(pk)",
}));

// "Increment if balance >= cost" — fails atomically if not enough
await ddb.send(new UpdateCommand({
  TableName: BALANCE_TABLE,
  Key: { user_id },
  UpdateExpression: "SET balance = balance - :cost",
  ConditionExpression: "balance >= :cost",
  ExpressionAttributeValues: { ":cost": cost },
}));
```

A `ConditionalCheckFailedException` is the success-as-failure signal — catch it and treat it as the
"already happened" branch.

## Atomic Counters

`UpdateExpression: "ADD count :delta"` is atomic. No need to read-modify-write:

```ts
await ddb.send(new UpdateCommand({
  TableName: COUNTERS,
  Key: { metric: "page_views" },
  UpdateExpression: "ADD #c :one",
  ExpressionAttributeNames: { "#c": "count" },
  ExpressionAttributeValues: { ":one": 1 },
}));
```

Concurrent increments compose correctly without a lock.

## BatchWriteItem: Chunk + Retry Unprocessed

`BatchWriteItem` accepts at most 25 items per call. It can also return `UnprocessedItems` if the
batch hits write capacity. Always chunk + retry:

```ts
const CHUNK = 25;
for (let i = 0; i < rows.length; i += CHUNK) {
  let unprocessed = { [TABLE]: rows.slice(i, i + CHUNK).map(toDeleteRequest) };
  let attempts = 0;
  while (unprocessed && Object.keys(unprocessed).length && attempts < 5) {
    const res = await ddb.send(new BatchWriteCommand({ RequestItems: unprocessed }));
    unprocessed = res.UnprocessedItems ?? {};
    attempts += 1;
  }
}
```

Naive `BatchWrite` callers leak rows under burst load. Don't.

## TTL For Ephemeral Data

Mark transient rows with `ttl: <epoch_seconds>`. DynamoDB sweeps expired rows (within ~48 hours) for
free. Use it for:

- Idempotency keys (4-day window for Stripe; 6-hour for Slack)
- WebSocket connection rows
- Rate-limit buckets
- Verification codes / one-time tokens
- Subscription rows for transient peers

The TTL attribute name is set per-table in `TimeToLiveSpecification` — typically just `ttl`.

## Streams For Fan-Out

Enable `StreamSpecification: NEW_AND_OLD_IMAGES` on any table whose mutations another system needs
to know about. A Lambda subscribed to the stream replaces:

- Cron jobs that scan for changes
- Application-layer dual writes to a second store
- "After-write" hooks scattered across handlers

Stream Lambdas must be idempotent — DynamoDB delivers at-least-once. They get retried with
exponential backoff and finally land in a DLQ.

## Pagination Always

Every Query / Scan loops `LastEvaluatedKey`:

```ts
async function queryAllItems<T>(params: QueryCommandInput): Promise<T[]> {
  const out: T[] = [];
  let lastKey: Record<string, unknown> | undefined;
  do {
    const res = await ddb.send(
      new QueryCommand({ ...params, ExclusiveStartKey: lastKey }),
    );
    out.push(...((res.Items as T[] | undefined) ?? []));
    lastKey = res.LastEvaluatedKey as Record<string, unknown> | undefined;
  } while (lastKey);
  return out;
}
```

Single-page Query results lie about completeness. Always loop, or apply an explicit `Limit` and
surface "there are more" to the caller.

## Hot-Partition Avoidance

A single PK that takes >3000 RCU or >1000 WCU per second hot-partitions and throttles, regardless of
table-level capacity. Defenses:

- Multi-tenant: `organization_id` partitioning naturally spreads load
- Single-tenant high-write workloads: append a write-shard suffix (`organization_id#<0-19>`) and
  Query each shard at read time
- Global counters: don't use one row; use sharded counters and aggregate

Watch CloudWatch's `ConsumedWriteCapacityUnits` per-partition to spot a hot key before it pages you.

## Don't Use Scans In Production

A Scan reads every row in the table. At 10k rows it's an annoyance; at 10M it's an outage. The two
legitimate Scan use cases:

- One-shot data migrations / backfills (run from a script, not a request handler)
- Nightly integrity walkers (audit chain verification, GDPR sweep)

Both should respect a `MAX_ROWS` cap and emit "I hit the cap" telemetry so an unbounded scan can't
silently exhaust memory.

## Single-Table vs Multi-Table

The "official" DynamoDB design pattern is single-table — one table holds every entity, distinguished
by `PK` / `SK` shapes. Pros: every query is a single Query. Cons: schemas are encoded in code, not
the table; new entities are easy but new queries on existing entities require GSIs or rewrites.

Multi-table is the simpler default. Use single-table when:

- You query across entity types in the same Query call
- You're running thousands of orgs and want to consolidate read capacity
- You have a stable, well-understood entity model

If unsure, start multi-table. It's easier to migrate towards single-table when access patterns
settle than the reverse.

## Common Smells

| Smell | Fix |
| ----- | --- |
| Query without `organization_id` in a multi-tenant table | Add it; or document the deviation in an allowlist |
| `as` cast on `res.Items` to a domain type | Validate at the boundary; DDB returns `Record<string, unknown>` |
| `BatchWriteCommand` with > 25 items | Chunk |
| `UpdateExpression` reading then writing the same attribute in code | Use atomic `ADD` / `SET` with `ConditionExpression` |
| `Scan` in a request handler | Replace with a Query against a GSI |
| GSI with `ProjectionType: ALL` and few rows accessing it | Switch to `KEYS_ONLY` or `INCLUDE` |
| TTL attribute set in code but not declared in `TimeToLiveSpecification` | Declare it; DDB silently ignores otherwise |
| Stream consumer that isn't idempotent | Add an idempotency key check or a conditional write |

## Skill Chain

1. **dynamodb-patterns** — this skill
2. **backend-patterns** — handler / repository shape
3. **security-review** — multi-tenant isolation, defense in depth
4. **aws-serverless-patterns** — Lambda integration, stream handlers

## Purpose

Principal-level DynamoDB design: single-table modelling with composite PK + GSI overload,
conditional writes for idempotency, BatchWriteItem chunking, item-collection size limits, TTL for
retention, Streams for change capture, tenant isolation, on-demand vs provisioned capacity choice,
transactional writes.

**Negative scope** (NOT what this skill covers):

- Relational schema (Postgres / MySQL) — see `postgres-patterns`
- Analytical query patterns — see `clickhouse-io`
- AWS Lambda + DDB triggers — see `aws-serverless-patterns`
- Generic backend service-layer wiring — see `backend-patterns`

## When NOT to use

- Workloads needing complex JOIN / aggregation queries (use Postgres or Athena)
- Strong-consistency multi-row transactions exceeding 100 items per TX (DDB transaction limit)
- Workloads with unpredictable / spiky access patterns where provisioned capacity model wastes
  budget (use on-demand only)
- Data with strong relational integrity needs (FKs, RLS-style policies)

## Standards Cited

- **Amazon DynamoDB Developer Guide** (`docs.aws.amazon.com/amazondynamodb/`) — canonical
- **AWS Well-Architected Framework — Data pillar** — design principles
- **The DynamoDB Book (Alex DeBrie)** — community-canonical single-table reference
- **CAP Theorem** + **PACELC** — consistency / availability trade-offs
- **OWASP ASVS 4.0.3 §4 (Access Control)** — tenant isolation
- **OWASP ASVS 4.0.3 §13.1 (Generic Web Service Security)** — input validation on item attributes
- **NIST SP 800-53 Rev 5 AC-3 (Access Enforcement)** — IAM `LeadingKeys` for tenant isolation
- **NIST SP 800-53 Rev 5 AC-6 (Least Privilege)** — per-table / per-index IAM scoping
- **CWE-22 (Path Traversal)** — applies to composite-key construction with user input
- **CWE-639 (Authorization Bypass via User-Controlled Key)** — tenant-id in PK
- **RFC 7232 (HTTP Conditional Requests)** — ETag + DDB version attribute for optimistic concurrency
- **AWS IAM Best Practices** — least privilege on DDB resources

## Anti-Patterns

| Pattern | Why bad | Correct alternative |
| --- | --- | --- |
| One table per entity ("table-per-class") | Loses single-table benefits; fan-out queries | Single table with overloaded PK / SK; GSI per access pattern |
| `Scan` operations on hot path | O(n) cost + RCU exhaustion | Use Query with PK; design GSI for the access pattern |
| `BatchWriteItem` of > 25 items | Silent partial failure | Chunk into batches of 25; retry `UnprocessedItems` |
| Conditional writes without idempotency key | Duplicate writes on retry | `ConditionExpression: attribute_not_exists(pk)` for upsert OR explicit idempotency key per `idempotency.md` |
| Streams + Lambda without batchSize tuning | Throttling + cold-start overhead | `batchSize=10`, `maximumBatchingWindowInSeconds=5`, `parallelizationFactor=10` |
| Hot partition key (e.g., shared timestamp prefix) | Single physical partition saturates | Add random suffix OR use composite that distributes |
| TTL set but cleanup-dependent logic in app | TTL deletion is async (up to 48h delay) | Treat TTL as eventual; verify state via item attribute, not "exists" |
| Provisioned capacity without auto-scaling | Throttling under load OR waste | On-demand for variable; provisioned + auto-scaling for stable |
| Cross-tenant access via shared PK | Data leakage on bug | Include `tenant_id` in PK; verify in IAM policy via `dynamodb:LeadingKeys` condition |
| `UpdateExpression` without `ExpressionAttributeNames` for reserved words | Cryptic errors | Always use `#name` + `:value` placeholders |

## Verification Checklist

- [ ] Access patterns documented + GSI designed per pattern (not "just in case")
- [ ] No Scan in hot-path code; Query everywhere
- [ ] BatchWrite chunks to 25; retries `UnprocessedItems`
- [ ] Conditional writes for idempotency on POST-style endpoints
- [ ] Streams enabled with appropriate `StreamViewType` (NEW_AND_OLD_IMAGES for audit)
- [ ] TTL configured; tested with `aws dynamodb update-time-to-live`
- [ ] Tenant isolation via PK prefix + IAM `LeadingKeys` condition
- [ ] Item size monitored (DDB 400KB limit; design for < 16KB typical)
- [ ] Backup strategy: PITR enabled OR scheduled export to S3
- [ ] CloudWatch alarms on `ConsumedReadCapacityUnits` + `ThrottledRequests`

## Cross-References

- `~/.claude/skills/aws-serverless-patterns/SKILL.md` — Lambda + DDB triggers
- `~/.claude/skills/backend-patterns/SKILL.md` — repository shape
- `~/.claude/skills/postgres-patterns/SKILL.md` — relational alternative
- `~/.claude/skills/clickhouse-io/SKILL.md` — OLAP alternative
- `~/.claude/rules-library/common/idempotency.md` — conditional-write pattern
- `~/.claude/rules-library/common/observability.md` — DDB metrics + alarms
- `~/.claude/agents/database-reviewer.md` — Council Division 9
- `~/.claude/agents/security-reviewer.md` — IAM + tenant isolation

## Why this skill exists

DynamoDB rewards single-table design and punishes relational reflexes: developers coming from RDBMS
create table-per-entity, run Scan on the hot path, and discover at scale that the system charges per
RCU and throttles under load. The patterns above codify the principal-level posture: single-table +
overloaded GSI + Query-everywhere + idempotency-via-conditional-write + tenant-isolated-via-IAM.
Apps following these defaults scale to millions of requests-per-second at predictable cost.

## Learning hooks

Per `~/.claude/rules/common/continuous-learning-mandate.md`:

**Signals to watch**:

- Cross-tenant scan / query without `tenant_id` PK prefix (multi-tenant isolation weakening)
- BatchWrite > 25 items in one call without chunking (DDB hard limit)
- Conditional write missing on idempotency-sensitive write (double-execute risk)
- Hot partition pattern emerges (single PK absorbs > 1000 RCU/s or 1000 WCU/s) — composite-key
  redesign needed
- GSI projected attributes set to ALL when only specific attrs are read (cost inflation)
- Stream consumer not handling `OLD_IMAGE` for tombstone-style deletes
- TTL attribute set in code but `TimeToLiveSpecification` not in IaC (DDB silently ignores)
- ScanIndexForward used to reverse query order (cheap; flag good usage)
- `Scan` operation in production code path (cost + latency anti-pattern; use Query)
- BatchGet returning UnprocessedKeys without retry-with-backoff

**Refinement candidates**:

- New access-pattern row when a new query shape appears (e.g., reverse-chronological by org)
- New conditional-write template when idempotency is required on a new operation class
- New GSI design rule when a cost overrun is traced to overprojection
- New cross-reference when a sister skill (aws-serverless-patterns, postgres-patterns,
  dynamodb-patterns) adds a related pattern

Attribution

NmorNmor
View sourceMore from Nmor →
SSkills DirectorySkills Directory

Ship a skill? Prove it's safe.

Free 120-pattern security scan, letter grade, and an embeddable README badge.

Submit a skill

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments (0)

No comments yet. Be the first to comment!

SSkills DirectorySkills Directory

Ship a skill? Prove it's safe.

Free 120-pattern security scan, letter grade, and an embeddable README badge.

Submit a skill

Related Skills

Caveman

Ultra-compressed communication mode that cuts output tokens while keeping technical accuracy. Levels: lite, full, ultra and the wenyan variants. Use for /caveman, "caveman mode", "talk like caveman", "be brief" or "less tokens".

1074701 votes

Hyperplan

Adversarial multi-agent planning skill. Self-orchestrates 5 hostile category members (unspecified-low, unspecified-high, deep, ultrabrain, artistry) via team-mode for ruthless cross-critique debate, distills only the defensible insights, then MANDATORILY hands the distilled insight bundle to the `plan` agent for executable plan formalization. Use when planning needs maximum rigor and surfacing of weak assumptions, blind spots, and over-engineering. Triggers: 'hyperplan', 'hpp', '/hyperplan', ...

695601 votes

Mcp Code Execution

Routes multi-tool workflows through MCP servers for large datasets and pipelines. Use when Bash tool overhead is limiting throughput on data-heavy tasks.

3351 votes

catchup

Recovers the conversation and failed tool calls of a previous Codex, Claude Code, Antigravity, Cline, Copilot CLI, Cursor, DeepSeek Harness, Kimi, OpenCode, Pi Agent, or ZCode session. Use when the user says "catch up", "what did the last session do", "get me up to speed", "I switched agents", asks to recover/summarize a previous session before continuing, or asks to diagnose or report a catchup failure. Do NOT use for the current conversation, git history, or any non-agent log.

691 votes

math-skill

A comprehensive mathematical reasoning skill for AI assistants — handles arithmetic to research-level problems with rigorous step-by-step reasoning, systematic verification, and transparent uncertainty handling

381 votes
View all in ai-agents →