New to Claude Skills? Learn how to install them →

aws on GitHub

Amazon DynamoDB

OfficialFree

Design and validate DynamoDB data layers efficiently.

by aws2.3k stars on aws/agent-toolkit-for-aws
3 views
Updated Aug 10, 2026
Get this skill

Free · Opens the source repo

What Amazon DynamoDB does

The Amazon DynamoDB skill provides a comprehensive framework for designing, reviewing, and debugging data layers in DynamoDB applications. It operates based on a set of design axioms that guide users through the process of defining access patterns, selecting appropriate partition and sort keys, and deciding between single-table and multi-table designs. The skill also covers configurations for Streams, Global Tables, and TTL, ensuring that users can create a robust and efficient data layer. By producing a defensible design along with a monthly cost estimate, this skill is ideal for developers and architects who need to ensure their DynamoDB implementations are both cost-effective and high-performing.

This skill is particularly useful during the design phase of a project, where it aids in identifying potential issues before they arise. Users can leverage the provided scripts to calculate costs, deploy models, benchmark performance, and generate reports, all while maintaining a clear separation between design and execution. The skill's pipeline is structured to allow for iterative design, enabling users to refine their models based on real-time feedback and performance metrics. This iterative approach is essential for optimizing DynamoDB configurations and ensuring that applications can scale effectively.

For those working with DynamoDB, this skill addresses common challenges such as debugging hot partitions, managing throttling, and handling unexpected costs. It is designed for both novice and experienced users who are looking to enhance their understanding of DynamoDB and improve their application’s architecture. The skill's host-agnostic nature allows it to be integrated into various environments, making it a versatile tool for any developer or designer focused on AWS services.

When to use it

Use this skill when designing, reviewing, or refactoring DynamoDB-backed applications to ensure optimal performance and cost efficiency.

When not to use it

This skill may not be suitable for simple data storage needs or when working with non-DynamoDB databases.

What you can build with it

Designing a New Application

Use the skill to create a comprehensive data layer design for a new application, ensuring all access patterns are accounted for.

Refactoring Existing Models

Leverage the skill to review and refactor existing DynamoDB models, identifying performance bottlenecks and cost inefficiencies.

Cost Management

Utilize the cost estimation feature to forecast monthly expenses associated with your DynamoDB usage before deployment.

How to install Amazon DynamoDB

View source

1. Install with the skills CLI

npx skills add aws/agent-toolkit-for-aws/amazon-dynamodb --agent claude-code

2. Or install it manually

Download the skill folder and drop it into ~/.claude/skills/ for all projects, or .claude/skills/ to scope it to one repo. Restart Claude Code so it picks up the new skill.

Anthropic's agentic coding CLI, and the reference implementation of Agent Skills. Drop a skill folder into ~/.claude/skills and Claude Code loads it automatically whenever a task matches the skill's description. Claude Code docs

Inside SKILL.md

Written by aws

DynamoDB Axioms

This document is a set of design axioms for DynamoDB applications. It is intended to be read by an agent with no other context about the application and used to produce a defensible data-layer design.

Resolving the skill's own paths

This skill is host-agnostic — it runs under Claude Code, Kiro, Codex, Cursor, a plain terminal, or CI. Where it lives on disk depends on the host (~/.claude/skills/…, ~/.kiro/…, ~/.codex/…, ~/.cursor/…, a repo checkout, anywhere). The agent's working directory is the user's project, not the skill bundle, so relative paths like scripts/calculate_costs.py will not resolve. Throughout this document, ${SKILL_DIR} means the absolute path of the directory that contains this SKILL.md file (the skill root, which holds scripts/ and references/).

Resolve ${SKILL_DIR} once per session, then reuse it. Pick the first method that works in your host:

  1. You already know it. You loaded SKILL.md from a path — ${SKILL_DIR} is the directory that file is in. This is the most reliable source; prefer it.

  2. An environment variable. If $DDB_SKILL_DIR is set, trust it.

  3. The bundled resolver (host-neutral, no host assumptions). It searches the common install roots and verifies the hit against sentinel files, so it never returns the wrong directory silently:

    # If you already know the path to the script, just run it directly:
    #   SKILL_DIR="$(sh /path/to/amazon-dynamodb/scripts/find_skill_dir.sh)"
    # If you don't, this host-neutral one-liner searches the common roots
    # (~/.claude, ~/.kiro, ~/.codex, ~/.cursor, ~/.config, ~/.local/share, $PWD):
    SKILL_DIR="$(find "$HOME" "$PWD" -maxdepth 7 -type f -name SKILL.md -path '*amazon-dynamodb*' 2>/dev/null \
                 | head -1 | xargs -I{} dirname {})"
    # Verify it before trusting it (sentinel check), then hand off to the resolver
    # for its loud-on-failure diagnostics:
    SKILL_DIR="$(sh "$SKILL_DIR/scripts/find_skill_dir.sh" 2>/dev/null || echo "$SKILL_DIR")"
    

    The resolver prints the verified skill root and exits 0, or prints nothing and exits non-zero with a fix-it message — so SKILL_DIR="$(sh …/find_skill_dir.sh)" is safe to trust when it succeeds. It is plain POSIX sh, so it behaves identically across hosts.

Once resolved, export it so every later command is a clean substitution and the scripts can also pick it up:

export DDB_SKILL_DIR="$SKILL_DIR"
python3 "$DDB_SKILL_DIR/scripts/calculate_costs.py" --model dynamodb_data_model.json --output cost_report.md

Internally the scripts locate their own siblings (other scripts, scripts/benchmark_lambda.py) relative to themselves, so you only ever need the root path — never each individual script path.

Rules:

  • Always invoke scripts with an absolute path (the $DDB_SKILL_DIR/… form). Do not cd into the skill directory — the user's working directory must stay put so their artifacts (dynamodb_data_model.json, cost_report.md, …) land where they expect.
  • If none of the three methods resolves the directory, stop and ask the user where the skill is installed rather than guessing. A wrong ${SKILL_DIR} produces confusing "file not found" failures downstream; one clarifying question is cheaper.

The pipeline at a glance

The skill is one tool per stage. The default path touches no AWS: most work is stage 1 (a design you can discuss and refine conversationally). Stage 2 (cost) runs on request or when the design is being finalized — not reflexively every turn. Stages 3–6 are a distinctly opt-in, heavyweight fork that creates real AWS resources and incurs a real bill; enter it only on explicit user agreement. Each stage's detailed contract is in the section named in the last column.

#StageCommand (after export DDB_SKILL_DIR=…)ReadsWritesAWS?Section
1Design(no script — you produce the access-pattern list + schema)(in-reply artifacts)noArtifacts to produce
2Costpython3 "$DDB_SKILL_DIR/scripts/calculate_costs.py" --model dynamodb_data_model.json --output cost_report.mddynamodb_data_model.jsoncost_report.mdnoCost estimation
3Deploypython3 "$DDB_SKILL_DIR/scripts/deploy_model.py" --model dynamodb_data_model.json --config benchmark_config.json --manifest-out created_resources.json --yes-deploymodel + configcreated_resources.jsonyesLive validation
4Benchmarkpython3 "$DDB_SKILL_DIR/scripts/benchmark_model.py" --model dynamodb_data_model.json --config benchmark_config.json --manifest created_resources.json --raw-out perf_raw.jsonl --summary-out perf_summary.jsonmodel + config + manifestperf_raw.jsonl, perf_summary.jsonyesLive validation
5Reportpython3 "$DDB_SKILL_DIR/scripts/generate_perf_report.py" --model dynamodb_data_model.json --summary perf_summary.json --output performance_report.mdmodel + summaryperformance_report.md, design_findings.jsonnoLive validation
6Teardownpython3 "$DDB_SKILL_DIR/scripts/generate_teardown.py" --manifest created_resources.json --out teardown.sh → review → bash teardown.sh --confirmmanifestteardown.shyes (on --confirm)Live validation step 6
Iteratepython3 "$DDB_SKILL_DIR/scripts/iterate_design.py" … (wraps 3→4→5→cost as one human-driven round)model + config + loop-state + manifestloop_state.json + the aboveyes (gated)Iterative design loop

Who reads what. You (the agent) read the compact artifacts: cost_report.md, design_findings.json, loop_state.json. The user reads performance_report.md. Never read perf_raw.jsonl (large) — it only feeds stage 5.

Consent gates. Stage 3+ needs --yes-deploy; the benchmark refuses to spend over cost_guardrail_usd without --allow-spend; teardown needs the user's attested review and intent before you run bash teardown.sh --confirm. Details in Live validation.

AWS access (MCP recommended, not required). Stages 3–6 talk to AWS (create tables, a Lambda, an IAM role, then benchmark and tear down). For the best experience with AWS API calls the AWS MCP server is recommended but not required — every script here uses boto3 directly and runs from a plain shell with standard AWS credentials (a profile, SSO, or environment credentials), so the skill works identically with or without the MCP server. Nothing in this skill assumes MCP-specific tools.

How to use these axioms

  1. Read the reference architecture first when the task is to design, review, or critique a full-app data layer (multi-entity schemas, multi-table layouts, end-to-end composition with streams/search/notifications). ${SKILL_DIR}/references/reference-architecture.md is a complete multi-tenant kanban task-board ("TaskBoard") SaaS on AWS backed by DynamoDB, with all of the surrounding pieces (Cognito, CloudFront, HTTP API, Lambdas, Streams, OpenSearch, AppSync Events, SQS/EventBridge, cascades, idempotency middleware) worked out and justified. The axioms tell you what must be true; the reference shows how these pieces fit together in practice. Not reading it on a multi-table design means you will miss patterns that are in the reference but hard to re-derive from axioms alone — idempotency middleware, phantom-upsert guards, AppSync channel authorization, the Notifications-as-EventBridge-not-table decision, cascade-delete via chunked BatchWriteItem. Skip this step only for small-scope questions — a single-table question, a query-cost calculation, a pointed debugging question.
  2. Produce the access-pattern list (next section) before applying any axiom below. Every modeling axiom assumes this list exists; an axiom that asks "is this pattern frequent?" or "what does this query return?" cannot be applied without it.
  3. Produce the artifacts listed under Artifacts to produce. These are the outputs of a design, not intermediate notes. The axioms shape the artifacts; the artifacts are what the agent hands back.
  4. Apply the Patterns section alongside the axioms. Patterns are not axioms — they are load-bearing implementation details that the reference made concrete, and that a design will need even when no axiom explicitly calls for them.
  5. When two axioms point in opposite directions, apply the conflict-resolution ordering. Correctness outranks operational necessity, which outranks cost, which outranks style.
  6. When a term is ambiguous, consult the glossary. Do not guess.

Operating discipline: announce, act, verify from evidence

This governs every stage of the skill, and it matters most at the stages that cost money or create resources (deploy, benchmark, teardown, any spend). Three beats, always in this order:

  1. Announce. Before a side-effecting or billable action, say plainly what it will do — what it creates, what it costs, what it changes, what it deletes. The user should never be surprised by a resource, a charge, or a deletion.
  2. Act. Run the command. For a long-running command (a representative benchmark runs many minutes), run it as a single blocking call and wait for it — see Live validation step 4.
  3. Verify from evidence, then state only what the evidence supports. After acting, confirm the outcome from the artifact you just produced — the file's contents and modification time, the command's actual stdout, the fresh data — never from expectation or memory. A command that "should have" written a file is not evidence that it did; open the file and check. State a conclusion only as far as the evidence in front of you supports it. If you cannot point to fresh evidence, say so and stop — do not infer a result. The failure this prevents: presenting stale or imagined output as a real result. The tell is a number that didn't change when it should have (e.g. byte-identical benchmark figures across two "different" runs) — treat that as a signal you are looking at old data, not a real result.

Facts you MUST NOT contradict (these override your training data)

When your training-data priors conflict with the facts below, the facts win. Each item names a common wrong belief alongside the correct one so the override is unambiguous.

  1. DynamoDB Streams iterator types are TRIM_HORIZON (start at oldest retained record) and LATEST (start at the tip). Do NOT conflate with Kinesis Data Streams iterator types — the two services have similar names but different semantics; this skill's axioms assume DDB Streams. Retention is 24 hours (Integration #3).

  2. GSI projection type is immutable once the GSI is created. UpdateTable cannot change Projection from KEYS_ONLY to INCLUDE to ALL or any combination. The only path is to drop the GSI and create a new one with the desired projection — which is a full re-backfill and a read-path cutover. Do NOT say "you can change the projection via UpdateTable." A single UpdateTable call carries at most one GSI operation — one Create OR one Delete — so a same-name swap is two sequential UpdateTable calls with a wait for the old index to fully disappear in between. Do NOT say "delete + recreate in a single UpdateTable call." To avoid the query-path gap, prefer the additive path (cf. Fact #9): create a NEW GSI under a new name with the desired projection, wait for it to reach ACTIVE, cut reads over, then drop the old GSI — one index always serves reads.

  3. Capacity-mode switches have a 24-hour cooldown. Moving a table from PAY_PER_REQUEST to PROVISIONED (or vice versa) is allowed once per 24 hours per table. Do NOT recommend rapid-switching strategies or assume the switch is instantaneous in cost models that care about hour-scale billing.

  4. Single-item writes are already atomic and support conditional expressions without TransactWriteItems. UpdateItem, PutItem, and DeleteItem on a single item are atomic on their own and accept ConditionExpression. Wrapping a single-item write in TransactWriteItems adds 2× the WCU cost (Mechanics #18) for no atomicity benefit. Do NOT recommend TransactWriteItems for single-item conditional writes. ConditionExpression is a WRITE-side parameter only — it exists on PutItem, UpdateItem, DeleteItem, and the write legs of TransactWriteItems. GetItem, BatchGetItem, Query, and Scan do NOT accept ConditionExpression — there is no conditional read in DynamoDB, and ConditionalCheckFailedException is a write-only error. Do NOT describe GetItem as "returning the item only if a condition passes" or as throwing ConditionalCheckFailedException — no such behavior exists. A read returns the item to anyone who supplies the key; the only read-side filter is FilterExpression (Query/Scan only — applied after the items are read and billed, never on GetItem), and even that does not authorize, it only narrows the result a caller already paid to read. The correct way to keep a caller from reading another tenant's item is to make the data unaddressable to them — partition-key the table on the authorization identifier (Data modeling #14) so a foreign key simply isn't in a partition the caller can reach — NOT to bolt a "conditional GetItem" on top.

  5. Maximum item size is 400 KB, hard cap. The 1 MB limit is the Query/Scan response-page cap, not an item cap. Do NOT quote 1 MB as the item limit. Items near 400 KB also cost more per write (WRU = 1 per 1 KB rounded up, Mechanics #18), so large items are expensive even before the cap bites.

  6. BatchGetItem and BatchWriteItem are NOT atomic. Partial failures are normal and returned via UnprocessedKeys (BatchGetItem) or UnprocessedItems (BatchWriteItem). The client must retry the unprocessed portion with exponential backoff. Do NOT describe batch operations as atomic or all-or-nothing — use TransactWriteItems when atomicity across multiple items is required (subject to Mechanics #14 bounds).

  7. Reserved Capacity applies to PROVISIONED capacity only, not to on-demand (PAY_PER_REQUEST). Do NOT recommend Reserved Capacity for on-demand tables — there is no such product. On-demand savings come from usage-based discounts or table-class selection (Standard vs Standard-IA), not reservations.

  8. A failed ConditionExpression still consumes write capacity. ConditionalCheckFailedException charges the same WCU as a successful write of the same shape. Do NOT claim that failed conditional writes are free or that the condition check happens "before" the write-cost is assessed. Plan cost models around expected failure rates (Mechanics #18 uses conditional_fail_rate for this reason).

  9. A GSI's key schema (partition key / sort key) is immutable once the GSI is created. UpdateTable can add a new GSI or drop an existing one, but it cannot alter the KeySchema of an existing GSI. Re-keying an index — including write-sharding a hot GSI partition key by adding a hash suffix — is therefore an additive migration, not a code-only change: create a new GSI with the new key → let it populate → cut reads over → drop the old GSI. A historical backfill is needed only when the new index must cover items that were already written and won't be touched again; a sparse or small in-flight index (e.g. one holding only active orders) populates from ongoing writes alone and needs no backfill. Do NOT describe a GSI key change as "just a code change" or "no schema migration."

These nine facts are not the full axiom set — they are the subset where LLM prior is most likely to be wrong. When a user's question intersects one of them, state the correct fact plainly and move on; do not hedge with "I think" or "typically."

The access-pattern list

Before touching a schema, enumerate every pattern the application must serve. For each pattern record:

  • A one-line description of what the caller is asking for.
  • Expected RPS (treat "unknown" as a design gap to close, per Mechanics #2).
  • Items returned per call and approximate item size in KB.
  • Consistency requirement (strong, eventual, or transactional).
  • Authorization scope — the identifier that must be verified before the call is permitted (per Data modeling #14).

The list is a numbered, ranked table. The rest of this document assumes it exists. Any modeling decision that cannot be traced back to an entry on this list is unjustified.

Per-entity operational-config inputs

This interview is required before proposing any table boundary. Producing a full multi-table design first and then backfilling "here are the assumptions I made" is a workflow violation, not a shortcut. The per-entity questions below drive the table-splitting decision via Data modeling #3; when the answers are agent-assumed rather than user-stated, the signal fires spuriously and the design ends up over-fragmented (or under-fragmented if the agent guessed "no divergence" to keep things simple). Ask first, then design.

Before grouping entities into tables, gather operational-config requirements from the user per entity (or per logical aggregate — a parent and its tightly-bound children can share one answer set). Do not assume these defaults silently, because Data modeling #3 uses operational-config divergence as a signal to split tables — if the divergence is agent-assumed rather than user-stated, the signal fires spuriously and the design ends up over-fragmented.

For each entity, ask:

  • Backup and recovery granularity. Does this entity need PITR? If so, what retention (default 35 days, can be shorter)? Would this entity ever be restored independently of other entities, or always together with them? (Independent-restore requirements force table separation per Data modeling #5.)
  • Streams consumers. Does any downstream system need change events for this entity — search indexing, analytics export, notifications, audit, CDC? Which stream view type (NEW_AND_OLD_IMAGES is the default per Integration #3)? A "no" here is a positive answer: no Streams consumer means Streams can stay disabled, which is cheaper and simpler.
  • Capacity mode. Does this workload's shape justify provisioned (sustained, predictable traffic over months, per Mechanics #19), or does on-demand remain the default? "Unknown" means on-demand.
  • TTL. Is there a per-item expiration attribute the application will set? If yes, the attribute is a Unix epoch second (per Patterns #3). If no, TTL stays off and items persist until deleted.
  • Encryption and IAM scope. Any non-default requirement — customer-managed KMS key, specific IAM boundary, cross-account resource policy? Default is AWS-owned KMS and standard IAM; divergence is an explicit answer.

Treat these as design inputs on par with RPS. A missing answer is a gap to close, not a value to guess. If the user says "same across all entities," record that and do not treat the entities as operationally divergent — co-location by Data modeling #1 is then unobstructed. If the user states real divergence, Data modeling #3 fires on real divergence and the tables split.

Per-entity attribute walkthrough (drives item size)

Item size is the second-largest driver of the cost estimate after RPS, and it's the place the estimate silently drifts worst. A Query declared as 20 items × 1,536 B but really returning 20 × 512 B triples the modeled cost against reality. Mechanics #2 says unknown RPS is a design gap; the same discipline applies to item size — an ungrounded guess for estimated_item_size_bytes is a design gap, not a safe default.

For each entity, before settling on a number, walk the attribute list with the user. Asking first is the preferred path; proceeding from inferred attributes is the fallback. Either way, the user has to see and sign off on the per-attribute breakdown before it becomes an input to the cost estimate — a silent fill-in is what makes item sizes drift 2–10×.

  1. Propose an attribute list grounded in the domain. For a Waypoint, that's waypoint_id, courier_id, lat, lng, recorded_at. For a Contract, it's firm_id, contract_id, title, status, body, created_by, created_at, updated_at.
  2. Per attribute, estimate bytes using these starting points:
    • IDs and short strings (ULIDs, UUIDs, slugs, enum values): ~40 B each. The generic S=100 heuristic in cost-model-schema.md is conservative for the free-tier storage path; for per-item size estimation, use realistic values.
    • Titles, display names, short descriptions: 100–300 B.
    • Long-form content (contract body, message body, serialized JSON aggregates): ask the user explicitly. Do not guess 4 KB or 50 KB without confirmation.
    • Numeric attributes: ~8 B.
    • Timestamps as ISO strings: ~25 B. As epoch numbers: ~8 B. (Mechanics #11.)
    • Boolean: ~1 B. Map/List: ~200 B per instance as a rough default, but ask if the user is storing a big blob inside a Map.
  3. Ask the corrections the user will know and you won't: "Does this item carry any denormalized parent data per Mechanics #10?" "Is there a free-text field whose length varies widely?" "Are you storing the full document or a summary?" Update the estimates from the answers.
  4. Sum the per-attribute estimates to derive the entity's estimated_item_size_bytes. For a Query that projects a subset (INCLUDE / KEYS_ONLY, or application-side projection), use a smaller number for the access-pattern's estimated_item_size_bytes — the bytes billed by DynamoDB are bytes actually read from the projected view, not the full item.
  5. If the user is uncertain on a specific attribute, label that attribute as an assumption in the artifact (same discipline as unknown RPS). Do not silently pick a number.
  6. Surface the full list in your response — always, regardless of whether this is an interactive conversation or a one-shot prompt. Emit a compact markdown table per entity with columns attribute | type | bytes | source (user or guess). This is a reply-shape requirement, not a dialog gate. In one-shot settings where there will be no follow-up turn, the table still goes in the response so the user sees exactly what you assumed — the call-out is how they catch a 3× overshoot on a body field before it contaminates every cost number downstream. Label every uncertain estimate "guess" explicitly; do not smuggle a guess in as a user-supplied number. Explicitly invite correction: "These are my guesses where noted — please correct any that are wrong." Even when the user said "just pick reasonable values and go," emit the table.

Calibration: for the reference Contracts-app example in cost-model-schema.md, Contract is ~2 KB (not 50 KB — the 50 KB value is the worst-case body size, not the typical), and Clause is ~512 B. If a declared estimated_item_size_bytes is more than 2× the sum of the named attributes and the user hasn't explained the gap, you're guessing — revisit.

A run that skips this walkthrough can drift by 2–10× on individual patterns. The live-validation step (below) will surface that drift, but you shouldn't need live validation to get the cost estimate in the right order of magnitude.

Artifacts to produce

Produce artifacts in the order listed below. Schema + per-pattern plan are the primary outputs; cost estimate (item 7) and live validation (item 8) come after the design exists, not instead of it. A response that leads with a cost analysis and buries the schema in an appendix has the dependency backwards — the user asked for a design, and the cost is a property of the design. The access-pattern list (item 1), schema (item 2), and per-pattern plan (item 3) must be visible and discussable in the reply before any cost numbers appear. Items 1–6 are the no-AWS design itself and are the default deliverable; item 7 (cost) is produced on request or at finalization (see Cost estimation); item 8 (live validation) is the opt-in AWS fork. Putting these artifacts only in dynamodb_data_model.json does not satisfy this — the user reads your prose, not the JSON. ❌ BAD reply shape (a real failure mode): a reply that opens "## Summary for the CFO — $1,019/month" with the schema living only in dynamodb_data_model.json on disk and the reply's only design content a trailing "artifacts produced" file list. ✅ GOOD: access-pattern list + per-table schema + per-pattern plan + per-entity byte table (per Per-entity attribute walkthrough step 6) rendered in the reply, then the cost summary, then "cost_report.md written."

A complete design hands back:

  1. The access-pattern list as above.

  2. A schema per table: primary key (named per Data modeling #7), GSIs with their key attributes and projection type, and the operational configuration (Streams, PITR, TTL, capacity mode, Global Tables replication, encryption, IAM scope — all per Data modeling #3).

  3. A per-pattern plan: for each access pattern in the list, the exact API call (GetItem, Query, or BatchGetItem, per Mechanics #16), the table or GSI it targets, the key conditions, the filter expressions if any, and the projected cost using the formulas in Mechanics #18.

  4. A fan-out topology: for each table with Streams enabled, the consumers (Lambda, EventBridge Pipe, Kinesis shim), the filters at the source (Integration #4), and the on-failure destinations and retry bounds (Integration #5).

  5. A list of deviations and their justification: any axiom or pattern not applied, with a stated reason.

  6. Idempotency and conditional-write guards: which routes use idempotency-key middleware (Patterns #1), which UpdateItems carry attribute_exists guards against phantom upserts (Patterns #2), and which PutItems carry attribute_not_exists guards against double-creation.

  7. A monthly cost estimate — produced on request or at finalization, not reflexively on every design turn. While the user is still exploring or refining the model, stay in design discussion and don't run the calculator each turn. Produce the estimate (via ${SKILL_DIR}/scripts/calculate_costs.py — see Cost estimation below) when the user asks what it costs, or when the design is being settled (they signal they're committing to it / taking it to review / want the numbers). When you do produce it, use the calculator — never inline arithmetic. Skip entirely for questions too narrow to have produced a full design (a single-query sizing, a debugging thread, a pointed mechanics question).

  8. A live validation (optional, on offer, last step): after the cost estimate, ask the user whether they want to deploy this schema to an AWS account they nominate and measure real per-operation capacity, latency, and GSI amplification against live DynamoDB. If yes, follow Live validation below. Skip for narrow questions, when the cost estimate was skipped, or when the user has no sandbox account. Unlike the cost estimate, this step creates real resources and incurs real charges, so both the offer and the consent must be explicit.

Conflict-resolution ordering

When axioms point in opposite directions, apply in this priority order:

  1. Correctness — authorization boundary alignment (Data modeling #14), consistency requirements, transactional atomicity, idempotency. A design that leaks data across tenants or serves stale data where strong consistency is required is wrong regardless of its other merits.
  2. Operational necessity — divergent PITR, Streams, capacity, or replication configuration (Data modeling #3), recovery granularity (Data modeling #5), per-partition throughput ceilings (Mechanics #3), transaction bounds (Mechanics #14). These are physical or service constraints; preference does not override them.
  3. Cost and performance — access-pattern co-location (Data modeling #1), dedicated GSIs (Data modeling #6), projection choice (Mechanics #7), cost formulas (Mechanics #18), capacity mode (Mechanics #19).
  4. Style and convention — naming (Data modeling #7), single-table vs. multi-table framing (Data modeling #11) absent other signal. The cheapest to override when a higher-tier axiom disagrees.

Two concrete examples:

  • Data modeling #1 (co-locate by shared access) vs. Data modeling #14 (partition by authorization boundary): #14 wins. If the natural access key and the authorization key differ, key on the authorization identifier and expose the alternate access via a GSI.
  • Data modeling #1 (co-locate) vs. Data modeling #3 (split on divergent operational config): #3 wins. Two entities sharing a read pattern but requiring different PITR retention or Streams consumers belong in separate tables.

Glossary

  • Access pattern — a request the application makes against the data layer, described by its key conditions, items returned, frequency, and consistency requirement. The atomic unit of DynamoDB design.
  • Aggregate — a cluster of entities that are read or written together. A single item, an item collection, or a set of items under different keys can each be an aggregate; the choice is the subject of Mechanics #1.
  • Item collection — the set of items sharing a single partition-key value. Queries against an item collection are constant-partition and cheap; cross-partition reads are not.
  • Identifying relationship — a data model in which a child entity is keyed by its parent's identifier plus its own. The child has no independent existence outside the parent.
  • Overloaded key — a partition or sort key whose value encodes a type prefix (e.g. USER#42, ORDER#42) so that one physical key holds multiple logical entity types.
  • Sparse GSI — a GSI whose indexed attribute is present on only a subset of base-table items, so the index projects only those items. Useful when an access pattern would otherwise filter out most items at read time.
  • GSI write amplification — the property that every write to a base-table item with projected GSI attributes produces one write per matching GSI, each billed in WCU.
  • LWW (last-writer-wins) — the conflict resolution strategy used by standard Global Tables: the write with the newest timestamp wins; earlier writes are silently discarded.
  • MRSC (Multi-Region Strong Consistency) — an opt-in Global Tables mode that provides strong consistency across replicas via consensus, at higher write latency and cost.
  • Hot partition — a partition receiving traffic beyond the per-partition throughput ceiling (Mechanics #3), causing throttling even when table-level capacity is available.
  • Poison pill — a record that a stream consumer cannot process successfully, which blocks forward progress on its shard until it is discarded, retried to exhaustion, or routed to an on-failure destination.
  • RCU / WCU — read and write capacity units; the provisioned-mode spelling of the per-operation throughput unit. The formulas in Mechanics #18 are written in RCU/WCU and apply identically to on-demand.
  • RRU / WRU — read and write request units: the on-demand (PAY_PER_REQUEST) spelling of the same per-operation unit, billed per request rather than per provisioned capacity-second. One RRU = one RCU of work and one WRU = one WCU of work — the consumption math in Mechanics #18 is identical; only the billing dimension differs. The cost references (cost-model-schema.md) use RRU/WRU because the calculator prices on-demand; the axioms and the performance report use RCU/WCU. They are the same quantity — do not treat a model's RRU/WRU figure and the report's RCU/WCU figure as different things.

Data modeling

  1. Co-locate data by shared access pattern, not by domain. Two entities belong in the same table only when an application request fetches or writes them together. A shared business domain — "user data," "billing data" — is not sufficient justification. Co-location without a shared query introduces coupling and yields no performance benefit.

  2. Treat table count as an output of the design, not a target. Do not optimize for one table, nor for one table per entity. The correct number of tables is whatever the access patterns produce. If the analysis surfaces three tables, ship three tables.

  3. Treat table-level configuration as both a modeling input and an interface declaration. Streams, point-in-time recovery, TTL, capacity mode, attached Kinesis streams, Global Tables replicas, encryption, and IAM scope all apply at the table level — they declare how the table participates in the broader system. When two entities require different operational settings — different PITR retention, different stream consumers, different replication regions, different capacity modes — that divergence is a primary signal that they belong in separate tables, not a secondary concern to be reconciled later.

  4. DynamoDB Streams provide two concurrent consumers, no native per-entity filtering, and a 24-hour retention window. These properties constrain downstream architecture as firmly as the key schema does. Decide the fan-out topology before finalizing table layout.

  5. Design for recovery granularity. Point-in-time recovery operates at the table level; partial restores are not supported. If two entities would never be recovered together, they should not share a table. The cost of an incident scales with the volume of unrelated data the restore has to carry.

  6. Prefer additional GSIs to overloaded keys. The constraints that historically motivated index overloading — a low per-table GSI ceiling, per-index provisioned capacity, and no on-demand mode — generally do not apply to modern DynamoDB, which supports many GSIs per table with shared capacity and on-demand billing (consult the current AWS service-quota docs for the exact per-table GSI limit). Choose a dedicated GSI per access pattern unless a specific, measured reason argues otherwise.

  7. Name keys for what they represent. Use customer_id, order_created_at, and OrdersByCustomer rather than PK, SK, and GSI1. Self-describing keys reduce onboarding time, make code reviewable without a schema reference, and let tooling introspect the model. Reserve generic overloaded keys for genuinely polymorphic hierarchies.

  8. Pre-join only entities that are read together in a single request. Item collections exist to satisfy one-shot queries such as "fetch parent with all children." If no access pattern reads two entities together, do not store them together. A pre-join without a corresponding read is coupling without benefit.

  9. Default to not sharing tables across service boundaries. A table shared between services forces teams to coordinate GSI allocation, key conventions, and schema changes. That coordination cost compounds as the table grows. Service ownership should imply table ownership.

  10. Plan for the model to change. Access patterns evolve. A table layout that requires a multi-table refactor to accommodate a new pattern is a liability. Prefer designs in which adding or modifying an access pattern is a localized change to a single table instead of changing the full data access layer.

  11. Apply single-table design as a tactic, not a default. Single-table design is appropriate for tightly bound parent-child hierarchies with predictable joint access — orders and line items, tenants and sub-resources. It is not appropriate as a global rule. The pattern's value is local; applying it globally produces the operational problems documented in production post-mortems.

  12. Move analytical workloads off the operational table. DynamoDB is an OLTP store. SQL-shaped queries, full-table aggregations, and ad-hoc BI belong in a downstream system populated by export or zero-ETL integrations. Do not reshape the operational table to serve analytics.

  13. Design around last-writer-wins in standard Global Tables, or opt into Multi-Region Strong Consistency (MRSC) when the domain cannot tolerate it. Standard Global Tables provide multi-region active-active replication with eventual consistency and last-writer-wins conflict resolution — they are not CRDTs. Domains that cannot tolerate LWW — inventory, balances, counters — have two options: application-level conflict avoidance (region-pinned writes, shard partitioning, conditional updates), or MRSC, an opt-in mode that provides strongly consistent reads and writes across a designated replica set via a consensus protocol. MRSC requires a three-region topology (two active replicas plus a witness), carries higher write latency and cost, and is configured per table. Cross-account replicas are supported in both modes and inherit the same consistency semantics — account boundaries are an IAM and ownership surface, not a consistency one. The choice between standard Global Tables and MRSC is a modeling decision: it determines what domains the table can host.

    A special case: a single hot item under contention — a flash-sale stock decrement where thousands of buyers converge on one SKU. Here the problem is throughput, not just consistency: writes to one item serialize internally, so you hit elevated latency and the per-partition WCU ceiling (Mechanics #3) before the partition's raw budget, and every failed conditional decrement still burns WCU (Fact #8). Region-pinning a single item does NOT help — it pins the contention to one region without removing it. The fix is a sharded counter: split the quantity across N sibling items (sku#shard0..sku#shardN as the partition key so they spread across partitions), decrement a random shard under a > 0 condition, retry another shard on failure, and scatter-read all shards to sum — sized to undersell rather than oversell (each shard enforces its own floor). Note this IS write-sharding the partition key (Mechanics #3), applied to sibling counter items; it is the contention fix, not a contradiction of it. Region-pinning and per-item conflict avoidance remain right for high-cardinality, low-per-item-contention data (per-account balances, where each user is their own partition key and contention is ~1), not for one contended row.

  14. Align the partition key with the authorization or tenancy boundary. The identifier you verify before permitting a read or write — tenant_id, account_id, board_id, whatever scopes the caller's access — should be the partition key of every table holding data under that scope. When authorization and lookup share a partition, a query against a partition the caller cannot prove access to simply returns nothing; when they diverge, a caller can pass a foreign child identifier and reach data across the authorization boundary (IDOR). Data that legitimately crosses tenants belongs in a separate table with its own access rules, not as a compromise on the main one.

    On any design review — and first of all when the user asks what the security problem is — evaluating whether the partition key aligns with the authorization/tenancy boundary is the FIRST check to make and to state, ahead of hot-partition, projection, or cost findings (per Conflict-resolution ordering, correctness outranks cost). The canonical hole: a partition key set to a client-supplied identifier (a review_id/order_id from the URL) instead of the server-derived authorization identifier (user_id, tenant_id, account_id) — that is IDOR. If the design is already aligned, say so and move on; if it diverges, lead with it and do NOT downgrade it to "add a ConditionExpression" — a per-call check is discipline, not a structural fix. When you explain why a ConditionExpression is not the fix, get the mechanism right: a write-side condition is opt-in (one forgotten call path = IDOR) and it does not cover reads at all — but the reason it does not cover reads is that GetItem/Query take no ConditionExpression in the first place (Fact #4), NOT that a "conditional GetItem returns the item / throws ConditionalCheckFailedException anyway." Do not invent a read-side condition mechanism to argue against it. The structural fix is the key schema (the foreign key is unaddressable), and reads stay safe because the caller cannot name a partition they don't own — state it that way.

Integration

  1. Ensure Streams consumers are idempotent or deduplicated by event identifier. DynamoDB Streams deliver at-least-once — handlers will see the same record twice after a Lambda timeout, a batch retry, or replay during an incident, and must produce the same outcome whether invoked once or many times.

    Two strategies are viable:

    • Idempotent handler: structure the operation so repeating it is a no-op. Conditional writes, set-based mutations (ADD to a set), and idempotency tokens on downstream APIs all qualify.
    • Explicit dedupe: record the eventID from the stream record in a dedupe store (a DynamoDB table with TTL is the usual choice) and skip records already present.

    Example (dedupe by eventID, TTL bounded to the redelivery window):

    def handler(event, context):
        for record in event["Records"]:
            try:
                dedupe_table.put_item(
                    Item={"event_id": record["eventID"], "ttl": int(time.time()) + 86400},
                    ConditionExpression="attribute_not_exists(event_id)",
                )
            except ClientError as e:
                if e.response["Error"]["Code"] == "ConditionalCheckFailedException":
                    continue  # already processed
                raise
            process(record)
    

    Dedupe on eventID, not on application identifiers like order_id — the same aggregate legitimately produces many events, and keying on the business identifier will drop valid change records.

  2. Use the transactional outbox pattern for reliable event publication. Write the state change and an outbox record in a single TransactWriteItems call; have a downstream consumer read the outbox via Streams and publish the event. This eliminates the "updated but did not emit" failure mode without resorting to two-phase commit.

  3. Default the stream view to NEW_AND_OLD_IMAGES. Zero-ETL integrations with OpenSearch, most change-data-capture consumers, and any logic that depends on diffs all require both images.

  4. Filter events at the source, not in the handler. Source-side filtering has two mechanisms — mention both when diagnosing or proposing a fix:

    • Lambda Event Source Mapping FilterCriteria — filters stream records before Lambda invocation. A handler that previously ran on 10M events/day but cares about 500k of them becomes a handler that runs 500k times, billed for 500k invocations instead of 10M. Filter shape (JSON pattern matching against the DynamoDB stream record): {"dynamodb": {"NewImage": {"status": {"S": ["shipped", "delivered"]}}}}.
    • EventBridge Pipes filters — the same source-side discarding, applied when the Pipe is the consumer. Use Pipes when the fan-out crosses multiple downstream targets (an event → Lambda + SQS + EventBridge bus), since the 2-concurrent-consumer limit on Streams (Data modeling #4) makes Pipes the common path for >2-consumer topologies.

    Both mechanisms eliminate invocation cost AND the work the handler would have done filtering. Inline filter logic inside the handler pays full invocation cost for every discarded event — a filter that drops 95% of traffic means 95% of invocations are pure waste. If the question is about source-side filtering, the answer names both mechanisms and routes the recommendation to the one that fits the consumer topology.

  5. Treat Event Source Mapping configuration as a reliability surface. BatchSize, MaximumBatchingWindowInSeconds, ParallelizationFactor, MaximumRetryAttempts, MaximumRecordAgeInSeconds, BisectBatchOnFunctionError, and on-failure destinations together define delivery semantics. Several defaults are actively dangerous: MaximumRetryAttempts and MaximumRecordAgeInSeconds both default to -1, meaning a single unprocessable record will block its shard for the full 24-hour stream retention and then disappear silently. Bound retries, bound record age, and configure an on-failure destination so poison records are quarantined rather than stalling the shard or vanishing. Leaving any of these at defaults is a deliberate choice, not a neutral one.

  6. Treat stream enablement as a durable interface commitment. Disabling a stream and re-enabling it produces a new stream ARN. Any Lambda Event Source Mapping or EventBridge Pipe bound to the old ARN becomes orphaned. Stream lifecycle is part of the table's public contract and should be managed accordingly.

  7. Distinguish TTL-driven deletes from user-driven deletes. DynamoDB delivers TTL deletions through Streams with userIdentity.principalId set to dynamodb.amazonaws.com. Consumers should branch on this attribute to route TTL events separately — typically to archival or audit pipelines — rather than treating them as ordinary user deletes.

  8. Match the analytical or search workload to the supported zero-ETL integration; do not reshape the operational table to serve either. DynamoDB is an OLTP store. The supported path from the operational table to any read-optimized system is a managed integration, not a Scan and not a bespoke GSI. Route by workload type:

    • Full-text, vector, geospatial, or fuzzy search → zero-ETL to Amazon OpenSearch Service. Initial load from a PITR snapshot, ongoing change capture via DynamoDB Streams, near real-time freshness (seconds). Requires PITR enabled on the source and Streams enabled with NEW_AND_OLD_IMAGES.
    • SQL analytics, BI, materialized views, data sharing → zero-ETL to Amazon Redshift. Initial load from a PITR export, ongoing change capture via incremental exports every 15–30 minutes. Requires PITR enabled on the source and KMS configured with an AWS-owned or customer-managed key (AWS-managed KMS is not supported).
    • Open-format data lake, Apache Iceberg, multi-engine analytics (Athena, EMR, Spark, Redshift), or ML feature stores → zero-ETL to Amazon SageMaker Lakehouse. Glue-orchestrated initial export plus incremental exports that write Iceberg tables to S3 or S3 Tables, typically 15–30 minutes fresh. Requires PITR enabled and a resource-based policy granting Glue the export actions.
    • Point-in-time batch dump with custom downstream processing and no freshness requirement → DynamoDB export to S3 (not branded zero-ETL, but the correct fallback when no zero-ETL target fits). Consumes no RCU.

    In every zero-ETL path above, PITR on the source table is a hard prerequisite. Do not approximate search with begins_with queries and bespoke GSIs. Do not scan the operational table for analytics. The integrations exist because these workloads compose with a column store, a search engine, or an Iceberg table and do not compose with a NoSQL key-value store.

    When one operational table feeds BOTH the Redshift (SQL/BI) and SageMaker Lakehouse (Iceberg) paths, prefer a single DynamoDB→Lakehouse Iceberg export and have Redshift read the same Iceberg tables (Redshift Spectrum / native Iceberg support) rather than running two parallel zero-ETL integrations — this avoids a second incremental export and the duplicate per-GB charge with no loss of SQL/join capability. Use two separate integrations only when the consumers genuinely need divergent freshness or isolation.

Mechanics

  1. Select aggregate tightness by weighing how often entities are read together against how often they are written independently. Three options — embed children in a single item, group them as an item collection under a shared partition key, or store them as separate aggregates — sit on a spectrum of how tightly parent and children are bound. No single threshold governs the choice. A high read correlation argues for co-location, but a write-heavy workload with large items pushes the opposite direction, since every update rewrites the full item. Item size and whether the child count is bounded matter as much as access frequency. Rule of thumb: if order line items are fetched with orders most of the time and the line count is bounded, consider embedding or an item collection with the order as the parent; if individual line items receive frequent updates in isolation, keep them separate so each write does not rewrite the whole parent. Selecting the wrong tier is the underlying mistake behind most "single-table design gone wrong" stories.

  2. Document RPS for every access pattern. Without a request rate, you cannot size partitions, choose between on-demand and provisioned capacity, or justify a GSI. An estimate grounded in business context is sufficient; an absent rate is not. Treat "unknown" as a design gap to be closed.

  3. Respect the per-partition throughput ceilings. A single partition supports up to 1,000 write capacity units and 3,000 read capacity units per second. Workloads that exceed these limits must shard the partition key — typically with a hash suffix for write-heavy traffic or a time bucket for sequential keys.

  4. Base-table key schemas allow exactly one HASH and at most one RANGE. When a base table requires a composite key, encode it as a concatenated string with a stable delimiter, such as tenant_id#user_id. GSIs support native multi-attribute keys — up to four attributes for the partition key and up to four for the sort key — with DynamoDB hashing the PK attributes together for distribution. Prefer native multi-attribute GSI keys over synthetic concatenated keys: items are written with natural attributes from the domain model, client code does not concatenate or parse, and adding a new multi-attribute GSI to an existing table requires no backfill of synthetic attributes. Use synthetic concatenated keys only when the number of components exceeds four or when an older table is already committed to the pattern.

  5. Multi-attribute GSI keys have strict query rules. On the partition-key side, every PK attribute must be constrained with equality — a GSI with PK (tenant_id, region) cannot be queried by tenant_id alone, and inequality operators are not allowed on any PK attribute. On the sort-key side, attributes must be constrained left-to-right in the order they are defined; a middle attribute cannot be skipped. Equality conditions must precede any inequality, and only one inequality is allowed — it must be the final condition in the key condition expression. BETWEEN, >, <, >=, <=, and begins_with all count as inequality. Violating these rules is the most common reason a GSI fails to satisfy its intended access pattern.

  6. Consider a sparse GSI when 50% or more of items would be filtered out. Indexing the presence of an attribute is materially cheaper than indexing all items and filtering at read time. Sparse indexes also reduce write amplification, since only items carrying the indexed attribute are projected.

  7. Project only the attributes the access pattern reads. An ALL projection roughly doubles storage cost and write amplification for every base-table update. Use ALL if read latency is important, or if your application is read heavy in comparison to writes. If item sizes are large or the table is write heavy, use INCLUDE, and if latency warrants it, use KEYS_ONLY with a reverse table lookup.

  8. Avoid using mutable attributes as GSI keys. DynamoDB implements a key-attribute change as a delete followed by an insert in the index, doubling the write cost for that update. Fields that change frequently should not appear as GSI partition or sort keys.

  9. Use identifying relationships when child access is dominantly in the context of the parent. When a child entity cannot exist independently of its parent, and fetches or updates of children typically carry the parent identifier, model the relationship on the base table with the parent identifier as the partition key and the child identifier as the sort key. This removes a GSI for the "list children by parent" pattern and typically halves write cost by eliminating the associated index amplification. The exception is a hot path that updates a single child by its own id without the parent — a webhook scoped to a line item, for example. UpdateItem requires the full primary key, so if callers routinely arrive with only order_item_id, the identifying-relationship schema forces a GSI lookup to resolve order_id on every write, negating the savings. In that case, invert: key the child by its own id and carry the parent on a GSI. The right shape follows the hot access pattern, not a style preference.

  10. Restrict denormalization to attributes that rarely change. Short-circuit copies — a user's display name on an order, a product SKU on a line item — are safe and eliminate additional reads. Copying mutable attributes turns every update into a multi-item fan-out and quickly outweighs the read benefit.

  11. Choose temporal encoding based on required sort behavior. Strings sort lexicographically; "10" precedes "2". Use ISO 8601 timestamps when natural string sort is desirable and human readability matters. Use Unix epoch numbers when compactness, arithmetic, or precision matters. Do not mix encodings within a table. When a key attribute (a table or GSI partition/sort key) is a number — an epoch timestamp, a numeric id — or binary, you MUST declare its type in the data model so the live deploy creates it correctly. DynamoDB types only key attributes, and it enforces that type at write time: a key left undeclared deploys as string (S), and the first real integer/binary write then fails with ValidationException — in production, not in the benchmark (the seed may pass while live writes fail). Declare the type in entities[].attributes[] as {"name": "order_date", "type": "N"}, or table-level in attribute_definitions as {"attribute_name": "order_date", "attribute_type": "N"} (both spellings are read by the deploy and the benchmark; an attribute_definitions entry wins on conflict). See ${SKILL_DIR}/references/cost-model-schema.md.

  12. Do not rely on TTL for time-sensitive expiration. TTL deletions are eventual — the background sweeper runs on a best-effort schedule and expired items can remain visible for hours, in practice up to ~48 hours past the TTL timestamp, until the sweeper removes them. TTL is appropriate for storage reclamation and cleanup; it is not appropriate for security-sensitive expirations such as sessions, tokens, entitlements, or real-time event triggers.

    Two consequences follow:

    • Filter on reads. Because an item can outlive its TTL until the sweeper runs, every read path that cares about expiration must check the TTL attribute itself — FilterExpression of #ttl > :now, or an application-side check. Do not assume an item returned by DynamoDB is logically current.
    • Pair with EventBridge Scheduler when timing matters. If a workload needs a precise action at expiration — revoke a session, fire a reminder, release a lock — create a one-time EventBridge Scheduler invocation at write time for the exact timestamp, and let that invocation do the work. TTL handles eventual cleanup; the scheduler handles the time-sensitive trigger. Streams can then propagate the actual TTL deletion to archival or audit (see Integration #7), but Streams is not the timing mechanism.

    The TTL attribute must be a Unix epoch value in seconds, not milliseconds.

  13. Enforce uniqueness with transactions, not application logic. Create a sibling lookup item — for example, UNIQUE#email#user@example.com — and write it together with the entity in a single TransactWriteItems call. A check-then-write sequence in application code has a race window; the transaction does not.

  14. Work within transaction bounds. TransactWriteItems is bounded by three hard limits — always state all three when the question is about transaction bounds, never just one or two. The bounds are independent; exceeding any single one rejects the transaction:

    • 100 items per transaction — total item count across the array.
    • 4 MB total payload — combined size of all items in the transaction. Do NOT omit this bound or fold it into the 100-item bullet; a transaction with 10 items at 500 KB each passes the 100-item bound but fails the 4 MB bound. The payload cap is 4 MB exactly — not 16 MB (that's BatchGetItem), not 1 MB (that's Query/Scan page cap), not 400 KB (that's max item size).
    • Single region — a transaction executes in one region only; it does NOT span Global Tables replicas. If callers in different regions need to participate in the same transaction, standard Global Tables is the wrong consistency model for the workload (use MRSC or restructure so each transaction is region-local).

    Operations that routinely exceed these limits indicate an aggregate boundary that is too coarse, not a database limitation to be worked around. When an import or bulk operation crosses the 100-item boundary, batch it into multiple transactions and handle partial-failure recovery explicitly (a job id, checkpointed progress, idempotent retry of each chunk).

    Count items, not records, against the 100-item bound. When one logical record requires multiple DynamoDB items — a contact plus a uniqueness sentinel per Mechanics #13 plus a counter increment — every item in the TransactWriteItems array counts. 3 items per contact means ~30–33 contacts fit per transaction, not 100. The 4 MB payload bound imposes a second ceiling independently; for records near 30 KB each, the 4 MB cap bites before the 100-item cap. Plan the chunk size against both bounds and pick the smaller.

    Bulk imports over the 100-item boundary are background jobs, not synchronous API calls. A loop of TransactWriteItems that pages through tens of thousands of records will exceed any reasonable request timeout (API Gateway 30s, Lambda 15min ceiling, browser request timeouts). Model the operation as a durable job: accept the request, enqueue a job record, drive the chunked execution from a worker (Step Functions, SQS + Lambda, batch job) with checkpointed progress so restart resumes mid-run, and report completion asynchronously. Do not treat "chunked transactions" as if they compose into a single synchronous operation.

  15. Default to eventually consistent reads. Strongly consistent reads cost twice as many RCU. Use them only at boundaries that require read-your-writes semantics; do not adopt them as a global default.

    When a caller updates an item and immediately needs the fresh value, prefer ReturnValues=ALL_NEW on the UpdateItem (or ALL_OLD / UPDATED_NEW / UPDATED_OLD variants as fit) over a follow-up read. The write returns the post-update item as part of its response at no extra RCU cost and with no consistency concerns — the write's return payload is authoritative by construction. A strongly-consistent GetItem after an UpdateItem pays 1 RCU and a second round-trip for information that was already available for free. This is the cheapest read-your-writes shape on DynamoDB.

  16. Resolve every access pattern to GetItem, Query, or BatchGetItem. Scan is appropriate for administrative operations, not for application read paths. A production access pattern that requires a scan reflects a modeling gap, not an acceptable query choice.

  17. Constrain every query. Specify sort-key ranges or filter conditions, compute expected page sizes, and paginate with LastEvaluatedKey. The constraint can come from the key structure itself — a parent partition that holds a bounded number of children (line items under one order, members of one team) is already constrained, and "return all under this parent" is a valid access pattern against it. What is not an access pattern is a query whose result size grows without bound as the dataset grows — "all orders for a customer" ove

This file is truncated. Read the full SKILL.md on GitHub.

Frequently asked questions about Amazon DynamoDB

Similar skills