amazon-dynamodb

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

DynamoDB Axioms

DynamoDB 设计公理

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.
本文档是一组用于 DynamoDB 应用的设计公理。它面向不了解该应用其他上下文的 Agent,供其用于产出有充分依据的数据层设计。

Resolving the skill's own paths

解析 skill 自身的路径

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:
    bash
    # 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:
bash
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.
本 skill 与宿主无关——可运行于 Claude Code、Kiro、Codex、Cursor、普通终端或 CI。它在磁盘上的位置取决于宿主(
~/.claude/skills/…
、
~/.kiro/…
、
~/.codex/…
、
~/.cursor/…
、仓库检出目录或任意位置)。Agent 的工作目录是用户的项目,而不是 skill 包,因此
scripts/calculate_costs.py
等相对路径无法解析。在本文档中,
${SKILL_DIR}
表示包含此 SKILL.md 文件的目录的绝对路径,即保存
scripts/
和
references/
的 skill 根目录。
**每个会话只解析一次
${SKILL_DIR}
,然后重复使用。**选择当前宿主下第一种可行方法:
  1. **你已经知道路径。**你是从某个路径加载 SKILL.md 的,那么
    ${SKILL_DIR}
    就是该文件所在目录。这是最可靠的来源,应优先采用。
  2. **环境变量。**如果设置了
    $DDB_SKILL_DIR
    ,信任它。
  3. 内置解析器(与宿主无关,不依赖任何宿主假设)。它会搜索常见安装根目录,并通过哨兵文件验证命中结果,因此绝不会静默返回错误目录:
    bash
    # 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")"
    解析器成功时输出经验证的 skill 根目录并以状态码 0 退出;失败时不输出内容、以非零状态退出并给出修复提示。因此,解析器成功时,
    SKILL_DIR="$(sh …/find_skill_dir.sh)"
    可以安全信任。它使用纯 POSIX
    sh
    ,所以在不同宿主中的行为完全一致。
解析完成后,导出该变量,使后续命令都能进行干净替换,脚本也可以自行读取它:
bash
export DDB_SKILL_DIR="$SKILL_DIR"
python3 "$DDB_SKILL_DIR/scripts/calculate_costs.py" --model dynamodb_data_model.json --output cost_report.md
在内部,脚本相对于自身查找同级脚本(例如
scripts/benchmark_lambda.py
),因此只需要根目录路径,永远不需要逐个指定脚本路径。
规则:
  • 始终使用绝对路径调用脚本,即
    $DDB_SKILL_DIR/…
    形式。不要
    cd
    到 skill 目录——用户的工作目录必须保持不变,这样生成的文件(如
    dynamodb_data_model.json
    、
    cost_report.md
    )才会落在用户预期的位置。
  • 如果三种方法都无法解析目录,停止并询问用户 skill 安装在哪里,不要猜测。错误的
    ${SKILL_DIR}
    会导致后续出现令人困惑的“file not found”错误;提出一个澄清问题成本更低。

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
2Cost
python3 "$DDB_SKILL_DIR/scripts/calculate_costs.py" --model dynamodb_data_model.json --output cost_report.md
dynamodb_data_model.json
cost_report.md
noCost estimation
3Deploy
python3 "$DDB_SKILL_DIR/scripts/deploy_model.py" --model dynamodb_data_model.json --config benchmark_config.json --manifest-out created_resources.json --yes-deploy
model + config
created_resources.json
yesLive validation
4Benchmark
python3 "$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.json
model + config + manifest
perf_raw.jsonl
,
perf_summary.json
yesLive validation
5Report
python3 "$DDB_SKILL_DIR/scripts/generate_perf_report.py" --model dynamodb_data_model.json --summary perf_summary.json --output performance_report.md
model + summary
performance_report.md
,
design_findings.json
noLive validation
6Teardown
python3 "$DDB_SKILL_DIR/scripts/generate_teardown.py" --manifest created_resources.json --out teardown.sh
→ review →
bash teardown.sh --confirm
manifest
teardown.sh
yes (on
--confirm
)
Live validation step 6
—Iterate
python3 "$DDB_SKILL_DIR/scripts/iterate_design.py" …
(wraps 3→4→5→cost as one human-driven round)
model + config + loop-state + manifest
loop_state.json
+ the above
yes (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.
该 skill 在每个阶段提供一种工具。**默认路径不会接触 AWS:大部分工作属于阶段 1,即可以 conversational 讨论和 refinement 的设计。**阶段 2(成本)仅在用户请求时或设计最终定稿时运行,不应每轮都自动执行。阶段 3–6 是明确选择加入的重型分支,会创建真实 AWS 资源并产生真实费用;只有在用户明确同意后才能进入。每个阶段的详细契约见最后一列所指定的章节。
#阶段命令(执行
export DDB_SKILL_DIR=…
后)
读取写入接触 AWS?章节
1设计(无脚本——由你产出访问模式列表和 schema)—(回复中的交付物)否需要产出的交付物
2成本
python3 "$DDB_SKILL_DIR/scripts/calculate_costs.py" --model dynamodb_data_model.json --output cost_report.md
dynamodb_data_model.json
cost_report.md
否成本估算
3部署
python3 "$DDB_SKILL_DIR/scripts/deploy_model.py" --model dynamodb_data_model.json --config benchmark_config.json --manifest-out created_resources.json --yes-deploy
model + config
created_resources.json
是实时验证
4基准测试
python3 "$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.json
model + config + manifest
perf_raw.jsonl
、
perf_summary.json
是实时验证
5报告
python3 "$DDB_SKILL_DIR/scripts/generate_perf_report.py" --model dynamodb_data_model.json --summary perf_summary.json --output performance_report.md
model + summary
performance_report.md
、
design_findings.json
否实时验证
6拆除
python3 "$DDB_SKILL_DIR/scripts/generate_teardown.py" --manifest created_resources.json --out teardown.sh
→ 审查 →
bash teardown.sh --confirm
manifest
teardown.sh
是(使用
--confirm
时)
实时验证步骤 6
—迭代
python3 "$DDB_SKILL_DIR/scripts/iterate_design.py" …
(将 3→4→5→成本合并为一个由人驱动轮次)
model + config + loop-state + manifest
loop_state.json
+ 上述文件
是(需授权)迭代设计循环
**谁读取什么。***你(Agent)*读取精简交付物:
cost_report.md
、
design_findings.json
、
loop_state.json
。用户读取
performance_report.md
。绝不读取
perf_raw.jsonl
(文件很大)——它只供阶段 5 使用。
**授权门槛。**阶段 3 及以后需要
--yes-deploy
;基准测试在没有
--allow-spend
时,若预计支出超过
cost_guardrail_usd
就会拒绝执行;拆除操作需要用户确认已审查脚本并明确表达执行意图,之后才能运行
bash teardown.sh --confirm
。详情见实时验证。
**AWS 访问(推荐 MCP,但非必需)。**阶段 3–6 会与 AWS 交互,包括创建表、Lambda、IAM role,以及执行基准测试和拆除。为了获得最佳 AWS API 调用体验,推荐使用 AWS MCP server,但并非必需——这里每个脚本都直接使用
boto3
,可通过普通 shell 和标准 AWS 凭据(profile、SSO 或环境凭据)运行,因此有无 MCP server 时 skill 的行为完全相同。本 skill 不依赖任何 MCP 专用工具。

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.
  1. 当任务是设计、审查或批评完整应用的数据层时,先阅读参考架构,包括多实体 schema、多表布局,以及与 streams、搜索、通知等能力的端到端组合。
    ${SKILL_DIR}/references/reference-architecture.md
    描述了一个完整的多租户 kanban 任务板(“TaskBoard”)SaaS:它运行在 AWS 上并以 DynamoDB 为后端,同时完整设计和论证了周边组件,包括 Cognito、CloudFront、HTTP API、Lambda、Streams、OpenSearch、AppSync Events、SQS/EventBridge、级联删除、幂等中间件。公理说明必须满足什么;参考架构展示这些部分在实践中如何组合。如果设计多表架构时跳过这一步,就会遗漏参考架构中已有但很难仅靠公理重新推导的模式,例如幂等中间件、防止幻影 upsert 的保护、AppSync channel 授权、将 Notifications 作为 EventBridge 而非 table 的决策,以及通过分块
    BatchWriteItem
    实现级联删除。只有小范围问题可以跳过此步骤,例如单表问题、查询成本计算或针对性调试问题。
  2. 在应用下方任何公理之前,先产出访问模式列表(见下一节)。每条建模公理都假定该列表已经存在;如果某条公理询问“这个模式是否频繁?”或“这个查询返回什么?”,没有该列表就无法应用。
  3. 产出需要产出的交付物一节列出的交付物。这些是设计的输出,而不是中间笔记。公理塑造交付物;交付物才是 Agent 最终交回的内容。
  4. 将模式一节与公理一起应用。模式不是公理——它们是承重的实现细节,参考架构已将其具体化;即使没有公理明确要求,完整设计也需要这些模式。
  5. 当两条公理指向相反方向时,应用冲突解决顺序。正确性高于运营必要性,运营必要性高于成本,成本高于风格。
  6. 术语含义不明确时查阅术语表。不要猜测。

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.
该纪律适用于 skill 的每个阶段,在会产生费用或创建资源的阶段尤其重要,例如部署、基准测试、拆除以及任何支出操作。始终按以下顺序完成三个步骤:
  1. **声明。**在执行有副作用或会产生费用的操作前,清楚说明将要做什么——创建什么、花费多少、改变什么、删除什么。用户不应意外发现资源、费用或删除操作。
  2. **执行。**运行命令。对于长时间运行的命令(例如一次有代表性的基准测试通常需要数分钟),应以单次阻塞调用运行并等待完成——见实时验证步骤 4。
  3. **依据证据验证,然后只陈述证据支持的内容。**执行后,应从刚生成的交付物确认结果——文件内容与修改时间、命令的实际 stdout、新鲜数据——绝不能依据预期或记忆。一个“应该”写入文件的命令并不构成其确实写入的证据;应打开文件并检查。只在眼前证据支持的范围内陈述结论。**如果无法指出新鲜证据,就直说并停止,不要推断结果。**它防止的失败是:把陈旧或想象中的输出当成真实结果。典型信号是某个本应变化的数字没有变化,例如两次“不同”运行产生逐字节相同的基准测试数值;应把它视为读取了旧数据,而非真实结果。

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."
当训练数据中的先验与以下事实冲突时,以这些事实为准。每一项都列出了一个常见错误认知及其对应正确事实,以便明确覆盖原有认知。
  1. **DynamoDB Streams 的迭代器类型是
    TRIM_HORIZON
    (从保留的最旧记录开始)和
    LATEST
    (从最新位置开始)。**不要与 Kinesis Data Streams 的迭代器类型混淆;两个服务名称相似,但语义不同,本 skill 的公理假设使用 DDB Streams。保留期为 24 小时(集成 #3)。
  2. GSI 创建后,其 projection type 不可变。
    UpdateTable
    无法把
    Projection
    从
    KEYS_ONLY
    改为
    INCLUDE
    或
    ALL
    ,也无法改成其他组合。唯一路径是删除 GSI,再按所需 projection 创建新 GSI——这需要完整回填和读路径切换。不要声称“可以通过
    UpdateTable
    修改 projection”。单次
    UpdateTable
    调用最多执行一个 GSI 操作——要么创建一个,要么删除一个——因此同名替换需要依次执行两次
    UpdateTable
    ,并在中间等待旧索引完全消失。不要声称“能在一次
    UpdateTable
    调用中删除并重建”。为避免查询路径中断,优先采用增量路径(参见事实 #9):以新名称创建具有所需 projection 的新 GSI,等待其达到
    ACTIVE
    状态,切换读取流量,然后删除旧 GSI——始终保留一个可用于读取的索引。
  3. **容量模式切换有 24 小时冷却期。**每张表只能在 24 小时内从
    PAY_PER_REQUEST
    切换到
    PROVISIONED
    一次,反向切换亦然。不要建议快速反复切换的策略,也不要在关注小时级计费的成本模型中假设该切换是立即生效的。
  4. 单项目写入本身已经具备原子性,并支持条件表达式,无需使用
    TransactWriteItems
    。对单个项目执行的
    UpdateItem
    、
    PutItem
    和
    DeleteItem
    自身就是原子的,并接受
    ConditionExpression
    。将单项目写入包装进
    TransactWriteItems
    会增加 2 倍 WCU 成本(机制 #18),却没有任何原子性收益。不要为单项目条件写入推荐
    TransactWriteItems
    。
    ConditionExpression
    仅是写入侧参数
    ——它适用于
    PutItem
    、
    UpdateItem
    、
    DeleteItem
    ,以及
    TransactWriteItems
    的写入操作。
    GetItem
    、
    BatchGetItem
    、
    Query
    和
    Scan
    均不接受
    ConditionExpression
    ——DynamoDB 中不存在条件读取,
    ConditionalCheckFailedException
    是仅限写入端的错误。不要把
    GetItem
    描述为“仅在条件满足时返回项目”,也不要声称它会抛出
    ConditionalCheckFailedException
    ——不存在这种行为。读取操作会向任何提供正确 key 的调用方返回项目;唯一的读取侧过滤机制是
    FilterExpression
    (仅用于 Query/Scan——在读取并计费之后应用,绝不适用于
    GetItem
    ),而且它也不提供授权能力,只会缩小调用方已经付费读取的结果。防止调用方读取其他租户项目的正确方法,是让这些数据对其不可寻址——按授权标识符对表进行分区(数据建模 #14),使外部 key 根本不属于调用方能够访问的分区——而不是在外部附加所谓的“条件 GetItem”。
  5. **项目大小上限是 400 KB,属于硬限制。**1 MB 是
    Query
    /
    Scan
    的响应分页上限,不是项目上限。不要把 1 MB 引用为项目限制。接近 400 KB 的项目每次写入的成本也更高(WRU = 每 1 KB 向上取整后为 1,机制 #18),因此即使尚未触及上限,大项目也很昂贵。
  6. **
    BatchGetItem
    和
    BatchWriteItem
    不是原子的。**部分失败属于正常情况,并通过
    UnprocessedKeys
    (BatchGetItem)或
    UnprocessedItems
    (BatchWriteItem)返回。客户端必须使用指数退避重试未处理部分。不要将批处理操作描述为原子或全有或全无操作——跨多个项目需要原子性时应使用
    TransactWriteItems
    ,并遵守机制 #14 的限制。
  7. **预留容量仅适用于
    PROVISIONED
    容量模式,不适用于按需模式(
    PAY_PER_REQUEST
    )。**不要为按需表推荐预留容量——不存在这种产品。按需模式下的节省来自用量折扣或 table class 选择(Standard 与 Standard-IA),而不是预留。
  8. 失败的
    ConditionExpression
    仍会消耗写入容量。
    ConditionalCheckFailedException
    与同形状的成功写入消耗相同的 WCU。不要声称失败的条件写入是免费的,也不要声称条件检查发生在写入成本计算之前。成本模型应考虑预期失败率;机制 #18 正是因此使用
    conditional_fail_rate
    。
  9. GSI 创建后,其 key schema(分区键/排序键)不可变。
    UpdateTable
    可以添加新 GSI 或删除现有 GSI,但不能修改现有 GSI 的
    KeySchema
    。因此,对索引重新设定 key——包括通过添加 hash suffix 对写入热点 GSI 分区键进行分片——属于增量迁移,而不是只改代码:创建采用新 key 的 GSI → 等待其填充 → 切换读取流量 → 删除旧 GSI。只有当新索引必须覆盖已经写入且以后不会再触碰的项目时,才需要历史回填;稀疏或规模较小的在途索引,例如仅包含活跃订单的索引,会通过持续写入自行填充,无需回填。不要将 GSI key 变更描述为“只需改代码”或“无需 schema 迁移”。
这九项事实并非完整公理集,而只是 LLM 先验最可能出错的部分。当用户问题涉及其中任何一项时,直接陈述正确事实并继续;不要含糊地说“我认为”或“通常”。

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.
在接触 schema 之前,枚举应用必须支持的每一种模式。对每种模式记录:
  • 一行说明调用方正在请求什么。
  • 预期 RPS(依据机制 #2,将“未知”视为需要解决的设计缺口)。
  • 每次调用返回的项目数以及项目的大致 KB 大小。
  • 一致性要求(强一致、最终一致或事务性)。
  • 授权范围——在允许调用前必须验证的标识符(依据数据建模 #14)。
该列表应采用编号并排序的表格。后续文档均假定它已经存在。任何无法追溯到该列表中某个条目的建模决策都缺乏依据。

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.
**在提出任何表边界之前,必须完成此访谈。**先产出完整多表设计,再补充“以下是我采用的假设”,属于流程违规,而不是捷径。下方针对各实体的问题通过数据建模 #3 驱动拆表决策;当答案由 Agent 推测而非用户明确提供时,信号会虚假触发,使设计过度碎片化,或者为了保持简单而错误推测“没有差异”并导致表过度合并。必须先询问,再设计。
将实体分组到各表之前,需要从用户处按实体或按逻辑聚合收集运维配置需求。一个父实体及其紧密绑定子实体可以共用同一组答案。不要静默采用以下默认值,因为数据建模 #3 会将运维配置差异视为拆表信号——如果差异是 Agent 推测的而非用户明确说明的,该信号会虚假触发并导致过度拆表。
针对每个实体,询问:
  • **备份和恢复粒度。**该实体是否需要 PITR?如果需要,保留期是多久(默认 35 天,也可以更短)?该实体是否可能独立于其他实体恢复,还是始终一起恢复?独立恢复要求会依据数据建模 #5 强制拆表。
  • **Streams 消费者。**是否有下游系统需要该实体的变更事件,例如搜索索引、分析导出、通知、审计或 CDC?使用哪种 stream view type(依据集成 #3,默认是
    NEW_AND_OLD_IMAGES
    )?这里的“否”也是一个明确答案:没有 Streams 消费者时,可以保持 Streams 关闭,从而更便宜、更简单。
  • **容量模式。**该工作负载的特征是否足以证明应采用 provisioned 模式(依据机制 #19,流量数月内持续且可预测),还是按需模式仍是默认选择?“未知”表示使用按需模式。
  • **TTL。**应用是否会设置逐项目过期属性?如果会,该属性是 Unix epoch 秒(依据模式 #3)。如果不会,TTL 保持关闭,项目将一直存在直至被删除。
  • **加密和 IAM 范围。**是否存在非默认要求,例如 customer-managed KMS key、特定 IAM 边界或跨账户 resource policy?默认使用 AWS-owned KMS 和标准 IAM;任何差异都应成为明确答案。
将这些设计输入视为与 RPS 同等重要。缺少答案意味着需要解决的缺口,而不是可以猜测的值。如果用户表示“所有实体都一样”,记录该结论,并且不要把这些实体视为运维配置存在差异——依据数据建模 #1,此时不存在阻碍共置的运维差异。如果用户说明存在真实差异,数据建模 #3 就会因真实差异而触发,相应拆表。

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.
项目大小是成本估算中仅次于 RPS 的第二大驱动因素,也是估算最容易悄悄发生偏差的地方。Query 声明为
20 items × 1,536 B
,但实际返回
20 × 512 B
,会使模型成本变成实际成本的三倍。机制 #2 规定未知 RPS 是设计缺口;对项目大小也应遵循同样纪律——没有依据的
estimated_item_size_bytes
推测属于设计缺口,而不是安全默认值。
针对每个实体,在确定数值之前,与用户一起逐项梳理属性列表。优先路径是先询问;根据推断属性继续执行是后备路径。无论采用哪种方式,用户都必须看到并确认逐属性拆解,然后才能将其用作成本估算输入——静默补值正是项目大小偏差达到 2–10 倍的原因。
  1. 提出基于领域语义的属性列表。例如 Waypoint 可包含
    waypoint_id
    、
    courier_id
    、
    lat
    、
    lng
    、
    recorded_at
    ;Contract 可包含
    firm_id
    、
    contract_id
    、
    title
    、
    status
    、
    body
    、
    created_by
    、
    created_at
    、
    updated_at
    。
  2. 按以下起点估算每项属性的字节数:
    • ID 和短字符串(ULID、UUID、slug、枚举值):每项约 40 B。
      cost-model-schema.md
      中通用的
      S=100
      启发式规则面向免费额度存储路径,较为保守;估算逐项目大小时应采用更贴近现实的数值。
    • 标题、显示名称、短描述:100–300 B。
    • 长文本内容(合同正文、消息正文、序列化 JSON 聚合):必须明确询问用户。未经确认,不要猜测为 4 KB 或 50 KB。
    • 数值属性:约 8 B。
    • ISO 字符串时间戳:约 25 B;epoch 数值:约 8 B(机制 #11)。
    • 布尔值:约 1 B。Map/List 每个实例粗略默认约 200 B,但如果用户会在 Map 中存储大型 blob,应进行询问。
  3. 询问只有用户知道、而你不知道需要修正的问题:“该项目是否依据机制 #10 携带反规范化父级数据?”“是否存在长度变化很大的自由文本字段?”“存储的是完整文档还是摘要?”根据答案更新估算。
  4. 将逐项估算相加,得出实体的
    estimated_item_size_bytes
    。对于只投影部分属性的 Query(
    INCLUDE
    /
    KEYS_ONLY
    ,或应用侧投影),访问模式的
    estimated_item_size_bytes
    应使用更小的数值——DynamoDB 收费依据是从 projected view 实际读取的字节数,而不是完整项目大小。
  5. 如果用户无法确定某项属性,在交付物中将该属性标记为假设;处理方式与未知 RPS 相同。不要静默选取数值。
  6. 始终在响应中展示完整列表,无论当前是交互式对话还是一次性提示。针对每个实体输出紧凑的 Markdown 表格,列为
    attribute | type | bytes | source (user or guess)
    。这是对回复形态的要求,而不是对话门槛。在一次性场景中没有后续轮次时,仍应在响应中给出该表,让用户清楚看到采用了哪些假设——明确列出估算值可以让用户在某个正文字段导致成本估算偏差 3 倍之前及时发现。每个不确定估算都必须明确标记为“guess”;不要把推测伪装成用户提供的数值。应明确邀请用户修正:“以上标注处是我的推测——请纠正任何不准确之处。”即使用户说“选合理值继续”,也必须输出该表。
校准示例:在
cost-model-schema.md
的参考 Contracts 应用中,
Contract
约为 2 KB,而不是 50 KB——50 KB 是正文大小的最坏情况,不是典型值;
Clause
约为 512 B。如果声明的
estimated_item_size_bytes
超过已命名属性总和的 2 倍,而用户没有解释差距,就说明该数值是猜测,应重新检查。
跳过此梳理的运行,单个访问模式的估算可能偏差 2–10 倍。实时验证步骤能够暴露这种偏差,但你本就不必等到实时验证后才让成本估算处于合理的数量级。

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
    UpdateItem
    s carry
    attribute_exists
    guards against phantom upserts (Patterns #2), and which
    PutItem
    s 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.
**按下述顺序产出交付物。**Schema 和逐模式计划是主要输出;成本估算(项目 7)和实时验证(项目 8)位于设计完成之后,而不是之前。如果回复以成本分析开头,却把 schema 埋在附录中,依赖关系就颠倒了——用户要求的是设计,而成本是设计的属性。访问模式列表(项目 1)、schema(项目 2)和逐模式计划(项目 3)必须在任何成本数字出现之前,就在回复中清晰呈现并可供讨论。项目 1–6 构成无需 AWS 的设计本身,也是默认交付内容;项目 7(成本)仅在用户请求或设计定稿时产出(见成本估算);项目 8(实时验证)是选择加入的 AWS 分支。仅将这些交付物放进
dynamodb_data_model.json
并不能完成任务——用户阅读的是你的说明文字,而不是 JSON。❌ 错误回复形态(一种真实失败模式):以“## 给 CFO 的摘要——每月 1,019 美元”开头,而 schema 只存在于磁盘上的
dynamodb_data_model.json
,回复中的设计内容仅剩末尾的“已生成交付物”文件列表。✅ 正确做法:在回复中呈现访问模式列表、逐表 schema、逐模式计划和逐实体字节表(依据按实体梳理属性步骤 6),然后给出成本摘要,再然后说明“已写入
cost_report.md
”。
完整设计应交回:
  1. 上述访问模式列表。
  2. 每张表的 schema:主键(依据数据建模 #7 命名)、GSI 及其 key 属性和 projection type,以及运维配置(Streams、PITR、TTL、容量模式、Global Tables 复制、加密、IAM 范围——全部依据数据建模 #3)。
  3. 逐模式计划:针对访问模式列表中的每种模式,给出确切 API 调用(
    GetItem
    、
    Query
    或
    BatchGetItem
    ,依据机制 #16)、目标表或 GSI、key 条件、如有则给出 filter expression,以及使用机制 #18 公式计算的预计成本。
  4. 扇出拓扑:针对每张启用了 Streams 的表,列出消费者(Lambda、EventBridge Pipe、Kinesis shim)、源端过滤器(集成 #4),以及失败目标和重试上限(集成 #5)。
  5. 偏差列表及其理由:列出未应用的任何公理或模式,并说明原因。
  6. 幂等性和条件写入保护:说明哪些路由使用 idempotency-key 中间件(模式 #1),哪些
    UpdateItem
    携带
    attribute_exists
    保护以防止幻影 upsert(模式 #2),哪些
    PutItem
    携带
    attribute_not_exists
    保护以防止重复创建。
  7. 月度成本估算——仅在用户请求或设计定稿时产出,而不是每轮设计都自动执行。当用户仍在探索或完善模型时,应继续设计讨论,不要每轮运行计算器。当用户询问成本,或设计即将敲定时(其信号包括准备提交、进入评审或希望固化数字),应通过
    ${SKILL_DIR}/scripts/calculate_costs.py
    产出估算(见下文成本估算)。不要以内联算术代替计算器。对于范围过窄、未形成完整设计的问题(单次查询规模估算、调试讨论或针对具体机制的提问),完全跳过估算。
  8. 实时验证(可选,作为最后一步主动提供):完成成本估算后,询问用户是否希望将该 schema 部署到其指定的 AWS 账户,并对真实 DynamoDB 测量每次操作的容量、延迟和 GSI 放大。如果用户同意,遵循实时验证一节。对于范围过窄、跳过成本估算或用户没有 sandbox 账户的问题,应跳过此步骤。与成本估算不同,此步骤会创建真实资源并产生真实费用,因此提议和授权都必须明确。

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.
当公理指向相反方向时,按以下优先级应用:
  1. 正确性——授权边界对齐(数据建模 #14)、一致性要求、事务原子性、幂等性。跨租户泄露数据,或在要求强一致时返回陈旧数据的设计,无论其他方面有何优点都是错误的。
  2. 运营必要性——存在差异的 PITR、Streams、容量或复制配置(数据建模 #3)、恢复粒度(数据建模 #5)、逐分区吞吐量上限(机制 #3)、事务限制(机制 #14)。这些是物理或服务约束,偏好不能覆盖它们。
  3. 成本和性能——访问模式共置(数据建模 #1)、专用 GSI(数据建模 #6)、projection 选择(机制 #7)、成本公式(机制 #18)、容量模式(机制 #19)。
  4. 风格和约定——命名(数据建模 #7)、在缺乏其他信号时采用单表还是多表的表述方式(数据建模 #11)。当更高层级的公理与其冲突时,它最容易让步。
两个具体示例:
  • 数据建模 #1(按共享访问共置)与数据建模 #14(按授权边界分区)冲突时,#14 胜出。如果自然访问 key 与授权 key 不同,应按授权标识符设置 key,并通过 GSI 暴露替代访问路径。
  • 数据建模 #1(共置)与数据建模 #3(按运维配置差异拆表)冲突时,#3 胜出。两个实体即使共享读取模式,但要求不同的 PITR 保留期或 Streams 消费者,就应放入不同表。

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.
  • 访问模式——应用针对数据层发起的一次请求,描述其 key 条件、返回项目数、频率和一致性要求。它是 DynamoDB 设计的原子单元。
  • 聚合——一起读取或写入的一组实体。单个项目、项目集合,或位于不同 key 下的项目集合,都可以成为聚合;如何选择正是机制 #1 的主题。
  • 项目集合——共享同一个分区键值的一组项目。针对项目集合的 Query 固定在一个分区内,成本很低;跨分区读取则不然。
  • 标识性关系——一种数据模型,其中子实体以父实体标识符加自身标识符作为 key。子实体脱离父实体无法独立存在。
  • 重载 key——其值编码了类型前缀的分区或排序键,例如
    USER#42
    、
    ORDER#42
    ,从而让一个物理 key 承载多种逻辑实体类型。
  • 稀疏 GSI——仅在部分基础表项目上存在索引属性的 GSI,因此索引只投影这些项目。当访问模式原本需要在读取时过滤掉大多数项目时,它会非常有用。
  • GSI 写放大——基础表项目每次写入时,只要包含被投影的 GSI 属性,每个匹配的 GSI 都会产生一次写入,并分别按 WCU 计费。
  • LWW(last-writer-wins)——标准 Global Tables 使用的冲突解决策略:时间戳最新的写入获胜,较早写入会被静默丢弃。
  • MRSC(Multi-Region Strong Consistency)——Global Tables 的一种可选模式,通过共识为副本提供强一致性,但会提高写入延迟和成本。
  • 热点分区——流量超过逐分区吞吐量上限(机制 #3)的分区,即使表级容量仍有剩余也会触发限流。
  • 毒丸——Streams 消费者无法成功处理的记录,会阻塞其所在分片的前进,直到该记录被丢弃、重试至耗尽,或被路由到失败目标。
  • RCU / WCU——读取和写入容量单位;这是 provisioned 模式下每次操作吞吐量单位的称法。机制 #18 中的公式以 RCU/WCU 编写,并同样适用于按需模式。
  • RRU / WRU——读取和写入请求单位:这是按需模式(
    PAY_PER_REQUEST
    )下同一种单次操作吞吐量单位的称法,按请求而非预置容量秒计费。1 RRU = 1 RCU 的工作量,1 WRU = 1 WCU 的工作量——机制 #18 中的消耗计算完全相同,只是计费维度不同。成本引用(
    cost-model-schema.md
    )使用 RRU/WRU,因为计算器为按需模式定价;公理和性能报告使用 RCU/WCU。它们表示同一数量——不要把模型中的 RRU/WRU 数值与报告中的 RCU/WCU 数值视为不同事物。

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.
  1. 按共享访问模式而非领域共置数据。只有当某个应用请求会一起读取或写入两个实体时,它们才应位于同一表。共享业务领域,例如“用户数据”或“计费数据”,并不足以构成共置理由。没有共享查询的共置只会引入耦合,无法带来性能收益。
  2. 将表数量视为设计输出,而不是目标。不要为单表或每实体一表而优化。正确的表数量就是访问模式自然产生的结果。如果分析得到三张表,就交付三张表。
  3. 将表级配置同时视为建模输入和接口声明。Streams、point-in-time recovery、TTL、容量模式、附加的 Kinesis stream、Global Tables 副本、加密和 IAM 范围都作用于表级——它们声明该表如何参与更大的系统。当两个实体要求不同的运维设置,例如不同 PITR 保留期、不同 Streams 消费者、不同复制区域、不同容量模式时,这种差异是它们应分属不同表的主要信号,而不是以后再协调的次要问题。
  4. DynamoDB Streams 提供两个并发消费者,不支持原生的逐实体过滤,并具有 24 小时保留窗口。这些属性对下游架构的约束与 key schema 同样严格。应在最终确定表布局前决定扇出拓扑。
  5. 围绕恢复粒度进行设计。Point-in-time recovery 作用于表级,不支持部分恢复。如果两个实体绝不会一起恢复,就不应共享同一表。事故成本会随恢复时必须携带的无关数据量而增长。
  6. 优先使用额外 GSI,而不是重载 key。历史上促使索引重载的限制——每表 GSI 数量上限较低、每索引独立预置容量、没有按需模式——通常不适用于现代 DynamoDB;现代 DynamoDB 支持每表多个 GSI 共享容量并按需计费。每表 GSI 的确切限制应查阅当前 AWS service-quota 文档。除非有具体且经过测量的理由,否则应为每种访问模式选择专用 GSI。
  7. 按 key 所代表的内容命名。使用
    customer_id
    、
    order_created_at
    和
    OrdersByCustomer
    ,而不是
    PK
    、
    SK
    和
    GSI1
    。自解释 key 能缩短上手时间、让代码脱离 schema 文档也可审查,并让工具能够内省模型。只有真正多态的层级结构才应保留通用的重载 key。
  8. 只预连接会在单次请求中一起读取的实体。项目集合的存在是为了支持“一次获取父实体及其所有子项”等单次查询。如果没有任何访问模式会一起读取两个实体,就不要将它们存放在一起。没有对应读取的预连接只有耦合,没有收益。
  9. 默认不跨服务边界共享表。两个服务共享一张表,会迫使团队协调 GSI 分配、key 约定和 schema 变更。随着表不断增长,这种协调成本会不断累积。服务所有权应意味着表所有权。
  10. 为模型变化做好规划。访问模式会演化。如果某种表布局为了适配新模式而必须重构整个数据访问层,就属于负担。应优先选择能够通过局部变更添加或修改访问模式的设计,而不是变更整个数据访问层。
  11. 将单表设计视为一种策略,而不是默认做法。单表设计适合绑定紧密且联合访问可预测的父子层级,例如订单与订单项、租户与子资源。它不适合作为全局规则。模式的价值是局部的;全局应用会产生生产事故复盘中记录的那些运营问题。
  12. 将分析型工作负载移出运营表。DynamoDB 是 OLTP 存储。SQL 形态查询、全表聚合和临时 BI 应放在由导出或 zero-ETL 集成填充的下游系统中。不要为了服务分析而重塑运营表。
  13. 围绕标准 Global Tables 中的 last-writer-wins 机制进行设计;当领域无法容忍该机制时,选择 Multi-Region Strong Consistency(MRSC)。标准 Global Tables 提供多区域 active-active 复制、最终一致性和 last-writer-wins 冲突解决——它们不是 CRDT。无法容忍 LWW 的领域,例如库存、余额、计数器,有两种选择:应用层冲突规避(区域固定写入、分片分区、条件更新),或使用 MRSC;后者是一种可选模式,通过共识协议在指定副本集内提供强一致读写。MRSC 需要三区域拓扑(两个 active 副本加一个 witness),具有更高的写入延迟和成本,并按表配置。两种模式都支持跨账户副本,并继承相同的一致性语义——账户边界属于 IAM 和所有权边界,而不是一致性边界。标准 Global Tables 与 MRSC 之间的选择是建模决策:它决定该表可以承载哪些领域。
    一个特殊情况:存在竞争的单热点项目,例如限时促销中的库存递减,数千名购买者同时汇聚到同一个 SKU。这里的问题是吞吐量,而不仅仅是一致性:对单个项目的写入会在内部串行执行,因此在达到分区原始预算之前,就会先遇到延迟升高和逐分区 WCU 上限(机制 #3);每次失败的条件递减仍会消耗 WCU(事实 #8)。将单个项目固定到某个区域无济于事——这只会把竞争固定在一个区域,并没有消除竞争。解决方案是分片计数器:把数量拆分到 N 个兄弟项目(以
    sku#shard0
    ..
    sku#shardN
    作为分区键,使其分散到不同分区),在
    > 0
    条件下递减随机分片,失败时重试其他分片,再执行 scatter-read 汇总所有分片——容量应按宁可少卖而非超卖进行设计(每个分片分别维护自己的下限)。这确实是在对分区 key 进行写分片(机制 #3),但应用于兄弟计数器项目;这是解决竞争的方案,并非与之矛盾。区域固定和逐项目冲突规避仍适用于高基数、逐项目竞争较低的数据,例如按账户划分的余额,其中每个用户都是自己的分区 key,竞争约为 1;不适用于单条竞争激烈的记录。
  14. 让分区 key 与授权或租户边界对齐。允许读写前需要验证的标识符,例如
    tenant_id
    、
    account_id
    、
    board_id
    ,无论它限定何种范围,都应作为所有保存该范围内数据的表的分区 key。当授权与查找共享一个分区时,针对调用方无法证明有权访问的分区进行查询只会返回空结果;当二者不同时,调用方可以传入外部子项标识符并访问授权边界之外的数据(IDOR)。真正跨租户的数据应放入带有独立访问规则的单独表,而不是在主表上作出妥协。
    在任何设计审查中——尤其当用户首先询问安全问题时——评估分区 key 是否与授权或租户边界对齐,都是必须首先执行并陈述的检查,并且要先于热点分区、projection 或成本发现(依据冲突解决顺序,正确性高于成本)。典型漏洞是:分区 key 使用客户端提供的标识符,例如 URL 中的
    review_id
    /
    order_id
    ,而不是服务端推导出的授权标识符,例如
    user_id
    、
    tenant_id
    、
    account_id
    ——这就是 IDOR。如果设计已经对齐,说明即可并继续;如果存在偏差,应首先指出,不能将其降级为“加一个
    ConditionExpression
    ”——逐调用检查只是纪律,不是结构性修复。解释为什么
    ConditionExpression
    不是解决方案时,机制必须准确:写入侧条件是选择性启用的(遗漏一个调用路径就会产生 IDOR),并且它完全不覆盖读取;但原因不是所谓“条件 GetItem 会返回项目/抛出
    ConditionalCheckFailedException
    ”,而是
    GetItem
    /
    Query
    根本不接受
    ConditionExpression
    (事实 #4)。不要虚构读取侧条件机制来论证其不可用。结构性修复是 key schema(外部 key 不可寻址),读取之所以安全,是因为调用方无法指定自己不拥有的分区——应当这样说明。

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):
    python
    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.
  1. 确保 Streams 消费者具备幂等性,或按事件标识符去重。DynamoDB Streams 提供 at-least-once 投递——Lambda 超时、批次重试或事故期间重放后,处理器会再次看到同一记录;无论调用一次还是多次,都必须产生相同结果。
    两种策略可行:
    • 幂等处理器:将操作组织为重复执行也不产生变化。条件写入、基于集合的修改(
      ADD
      到 Set),以及下游 API 的幂等 token 都符合要求。
    • 显式去重:把 stream 记录中的
      eventID
      写入去重存储(通常使用带 TTL 的 DynamoDB 表),并跳过已存在的记录。
    示例(按
    eventID
    去重,TTL 受重投窗口限制):
    python
    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)
    应按
    eventID
    去重,而不是按
    order_id
    等应用标识符去重——同一聚合可以合法地产生许多事件,按业务标识符去重会丢失有效变更记录。
  2. 使用 transactional outbox 模式可靠发布事件。在单次
    TransactWriteItems
    调用中同时写入状态变更和 outbox 记录;由下游消费者通过 Streams 读取 outbox 并发布事件。这样无需两阶段提交,即可消除“已更新但未发出”的失败模式。
  3. 默认使用
    NEW_AND_OLD_IMAGES
    stream view。面向 OpenSearch 的 zero-ETL 集成、大多数 change-data-capture 消费者,以及任何依赖差异比较的逻辑,都需要新旧两份镜像。
  4. 在源端过滤事件,而不是在处理器内过滤。源端过滤有两种机制——诊断或提出修复方案时必须同时提及:
    • Lambda Event Source Mapping
      FilterCriteria
      ——在调用 Lambda 之前过滤 stream 记录。一个原本每天处理 1000 万事件、但实际只关心其中 50 万事件的处理器,将执行 50 万次并按 50 万次调用计费,而不是 1000 万次。过滤形态为针对 DynamoDB stream 记录的 JSON 模式匹配:
      {"dynamodb": {"NewImage": {"status": {"S": ["shipped", "delivered"]}}}}
      。
    • EventBridge Pipes filters——当 Pipe 是消费者时,采用相同的源端丢弃机制。当扇出跨越多个下游目标时,例如一个事件同时进入 Lambda、SQS 和 EventBridge bus,应使用 Pipes;因为 Streams 只有两个并发消费者(数据建模 #4),超过两个消费者的拓扑通常需要 Pipes。
    两种机制既消除调用成本,也消除处理器原本需要执行的过滤工作。在处理器内部编写内联过滤逻辑,会为每个被丢弃事件支付完整调用成本——丢弃 95% 流量的过滤器意味着 95% 的调用纯属浪费。如果问题涉及源端过滤,应指出这两种机制,并根据消费者拓扑选择合适方案。
  5. 将 Event Source Mapping 配置视为可靠性接口。
    BatchSize
    、
    MaximumBatchingWindowInSeconds
    、
    ParallelizationFactor
    、
    MaximumRetryAttempts
    、
    MaximumRecordAgeInSeconds
    、
    BisectBatchOnFunctionError
    和失败目标共同定义投递语义。多个默认值存在实际危险:
    MaximumRetryAttempts
    和
    MaximumRecordAgeInSeconds
    默认均为
    -1
    ,意味着一个无法处理的记录会阻塞其分片整整 24 小时 stream 保留期,随后静默消失。应限制重试次数和记录存活时间,并配置失败目标,使毒丸记录被隔离,而不是阻塞分片或无故消失。保留默认值必须是一项经过明确选择的行为,而不是中立状态。
  6. 将启用 stream 视为长期接口承诺。禁用后重新启用 stream 会产生新的 stream ARN。任何绑定旧 ARN 的 Lambda Event Source Mapping 或 EventBridge Pipe 都会成为孤立资源。stream 生命周期是表对外契约的一部分,应据此管理。
  7. 区分由 TTL 触发的删除与用户发起的删除。DynamoDB 通过 Streams 投递 TTL 删除时,
    userIdentity.principalId
    为
    dynamodb.amazonaws.com
    。消费者应根据该属性分支,将 TTL 事件路由到单独流程,通常是归档或审计管道,而不是将其视为普通用户删除。
  8. 让分析或搜索工作负载匹配受支持的 zero-ETL 集成;不要为了服务它们而重塑运营表。DynamoDB 是 OLTP 存储。从运营表到任何读优化系统的受支持路径是托管集成,而不是
    Scan
    或定制 GSI。按工作负载类型进行路由:
    • 全文、向量、地理空间或模糊搜索 → zero-ETL 到 Amazon OpenSearch Service。通过 PITR snapshot 完成初始加载,通过 DynamoDB Streams 持续捕获变更,近乎实时的新鲜度(秒级)。要求源表启用 PITR 和 Streams,并使用
      NEW_AND_OLD_IMAGES
      。
    • SQL 分析、BI、物化视图、数据共享 → zero-ETL 到 Amazon Redshift。通过 PITR export 完成初始加载,通过每 15–30 分钟一次的增量 export 捕获变更。要求源表启用 PITR,并使用 AWS-owned 或 customer-managed key 配置 KMS(不支持 AWS-managed KMS)。
    • 开放格式数据湖、Apache Iceberg、多引擎分析(Athena、EMR、Spark、Redshift)或 ML feature store → zero-ETL 到 Amazon SageMaker Lakehouse。由 Glue 编排初始导出和增量导出,将 Iceberg 表写入 S3 或 S3 Tables,通常每 15–30 分钟更新一次。要求启用 PITR,并配置授予 Glue 导出操作权限的 resource-based policy。
    • 无新鲜度要求、采用自定义下游处理的时点批量导出 → DynamoDB export 到 S3(不属于 zero-ETL 品牌,但在没有合适的 zero-ETL 目标时,这是正确后备方案)。不消耗 RCU。
    在上述每条 zero-ETL 路径中,源表启用 PITR 都是硬性前提。不要用
    begins_with
    查询和定制 GSI 近似实现搜索,也不要扫描运营表执行分析。之所以提供这些集成,是因为这些工作负载适合与列式存储、搜索引擎或 Iceberg 表组合,而不适合与 NoSQL key-value 存储组合。
    当一张运营表同时为 Redshift(SQL/BI)和 SageMaker Lakehouse(Iceberg)路径提供数据时,优先使用单个 DynamoDB→Lakehouse Iceberg export,并让 Redshift 读取同一批 Iceberg 表(Redshift Spectrum/原生 Iceberg 支持),而不是运行两个并行 zero-ETL 集成——这样可以避免第二次增量 export 和重复的每 GB 费用,同时不损失 SQL/join 能力。只有当消费者确实需要不同的新鲜度或隔离方式时,才使用两个独立集成。

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" over years, "all events in a log" — with no range, no page target, and no termination condition. An unbounded result set over a growing collection is a design gap, not a query choice.
  18. Compute the cost of each access pattern. Read pricing depends on the API:
    • GetItem / BatchGetItem: bills per item returned, rounded up to 4 KB. Eventually consistent (default) = 0.5 RCU per 4 KB per item; strongly consistent = 1 RCU per 4 KB; transactional = 2 RCU per 4 KB.
      BatchGetItem
      offers no per-request discount — each item is billed independently.
    • Query: bills on the total size of items matching the key condition, rounded up to 4 KB, at the same per-4 KB rate as
      GetItem
      .
      FilterExpression
      applies after billing — a filter that discards 90% of matches does not reduce cost by 90%.
    • Scan: bills on the total size of items examined — the portion of the table or GSI actually read — again before any filter is applied. This is the underlying reason
      Scan
      is the wrong choice on a hot path.
    Write pricing:
    WCU = frequency × ⌈item_size / 1 KB⌉ × copies_updated
    .
    UpdateItem
    is billed on
    max(before, after)
    item size
    , not the delta and not just the post-update size — a shrinking update still pays for the larger pre-update size rounded up. Transactional writes cost 2×.
    ConditionalCheckFailedException
    still consumes the same WCU as a successful write of that shape; plan
    conditional_fail_rate
    into the estimate.
    If these formulas cannot be written down for a given pattern, the pattern is not specified precisely enough to be modeled.
  19. Use on-demand capacity by default; switch to provisioned on evidence. On-demand removes a class of capacity-related incidents and is the correct default for new or variable workloads. At sustained high throughput with predictable patterns, provisioned capacity with autoscaling delivers a lower unit cost. The switch should be triggered by measured traffic, not anticipation.
  1. 根据实体一起读取的频率与独立写入的频率权衡,评估聚合绑定紧密程度。三种选择——将子项嵌入单个项目、将它们组织为共享分区 key 下的项目集合,或作为独立聚合存储——构成一个连续谱。不存在单一阈值决定选择。高读取相关性支持共置,但写入频繁且项目较大时会产生相反效果,因为每次更新都要重写整个项目。项目大小和子项数量是否有界,与访问频率同样重要。经验法则:如果订单行项目通常与订单一起获取,且行数有界,可考虑将它们嵌入订单项目或放入以订单为父项的项目集合;如果各行项目经常被单独频繁更新,则应保持独立,避免每次写入都重写整个父项。选错层级,是大多数“单表设计失败”故事背后的根本错误。
  2. 为每种访问模式记录 RPS。没有请求速率,就无法确定分区容量、无法在按需与预置容量之间选择,也无法为 GSI 提供依据。基于业务上下文的估算已经足够;完全缺失则不够。将“未知”视为需要解决的设计缺口。
  3. 遵守逐分区吞吐量上限。单个分区最多支持每秒 1,000 个写入容量单位和 3,000 个读取容量单位。超过这些限制的工作负载必须对分区 key 进行分片——写入密集流量通常使用 hash suffix,顺序 key 通常使用时间桶。
  4. 基础表 key schema 恰好允许一个
    HASH
    和最多一个
    RANGE
    。当基础表需要复合 key 时,将其编码为使用稳定分隔符的拼接字符串,例如
    tenant_id#user_id
    。GSI 支持原生多属性 key——分区 key 最多四个属性,排序 key 也最多四个属性,DynamoDB 会将 PK 属性一起哈希以实现分布。优先使用原生多属性 GSI key,而不是合成拼接 key:项目可直接使用领域模型中的自然属性写入,客户端代码无须拼接或解析,向现有表添加新的多属性 GSI 时,也不需要回填合成属性。仅当组件数量超过四个,或旧表已经采用该模式时,才使用合成拼接 key。
  5. 多属性 GSI key 有严格的查询规则。在分区 key 一侧,每个 PK 属性都必须使用等值条件约束——PK 为 (
    tenant_id
    ,
    region
    ) 的 GSI 不能只按
    tenant_id
    查询,并且任何 PK 属性都不允许使用不等式运算符。在排序 key 一侧,必须按照定义顺序从左到右约束属性;不能跳过中间属性。等值条件必须位于任何不等式之前,并且只允许一个不等式——它必须是 key condition expression 中的最后一个条件。
    BETWEEN
    、
    >
    、
    <
    、
    >=
    、
    <=
    和
    begins_with
    都算作不等式。违反这些规则是 GSI 无法满足预期访问模式的最常见原因。
  6. 当 50% 或更多项目会被过滤掉时,考虑使用稀疏 GSI。索引某个属性的存在,比索引所有项目再在读取时过滤要便宜得多。稀疏索引还可减少写放大,因为只有携带索引属性的项目才会被投影。
  7. 只投影访问模式实际读取的属性。每次基础表更新时,
    ALL
    projection 大致会翻倍存储成本和写放大。如果读取延迟很重要,或者应用读取远多于写入,应使用
    ALL
    。如果项目较大或写入密集,应使用
    INCLUDE
    ;如果延迟确有需要,可使用
    KEYS_ONLY
    并配合反向表查找。
  8. 避免使用可变属性作为 GSI key。DynamoDB 会把 key 属性变更实现为索引中的一次删除加一次插入,使该次更新写入成本翻倍。经常变化的字段不应出现在 GSI 分区或排序 key 中。
  9. 当子项访问主要发生在父项上下文中时,使用标识性关系。当子实体无法脱离父实体独立存在,并且子项的读取或更新通常携带父实体标识符时,在基础表中以父标识符作为分区 key、子标识符作为排序 key 来建模该关系。这消除了“按父项列出子项”模式所需的 GSI,并且通常可消除相关索引放大,将写入成本减半。例外情况是热点路径只按子项自身 ID 更新子项,例如范围限定到某个订单项的 webhook。此时
    UpdateItem
    需要完整主键,如果调用方通常只携带
    order_item_id
    ,标识性关系 schema 会强制每次写入先通过 GSI 查找
    order_id
    ,抵消节省。在这种情况下应反转设计:以子项自身 ID 为 key,并通过 GSI 携带父项。正确形态取决于热点访问模式,而不是风格偏好。
  10. 仅对很少变化的属性进行反规范化。短路副本是安全的,例如订单上的用户显示名称、订单项上的商品 SKU,因为它们消除了额外读取。复制可变属性会令每次更新都变成多项目扇出,很快便超过读取收益。
  11. 根据所需排序行为选择时间编码。字符串按字典序排序;
    "10"
    排在
    "2"
    前面。需要自然字符串排序和人类可读性时,使用 ISO 8601 时间戳;需要紧凑性、算术运算或精度时,使用 Unix epoch 数值。不要在同一张表中混用编码。**当 key 属性(表或 GSI 的分区/排序 key)是数值——例如 epoch 时间戳或数字 ID——或 binary 时,必须在数据模型中声明其类型,以便实时部署时正确创建。**DynamoDB 仅对 key 属性强制类型,并在写入时检查:未声明的 key 会以字符串(
    S
    )部署,随后第一次写入真实整数或 binary 时便会因
    ValidationException
    失败——而且是在生产环境中失败,基准测试阶段未必暴露,因为 seed 数据可能通过而实时写入失败。在
    entities[].attributes[]
    中声明
    {"name": "order_date", "type": "N"}
    ,或在表级
    attribute_definitions
    中声明
    {"attribute_name": "order_date", "attribute_type": "N"}
    (部署和基准测试都会读取这两种写法;发生冲突时以
    attribute_definitions
    为准)。参见
    ${SKILL_DIR}/references/cost-model-schema.md
    。
  12. 不要依赖 TTL 实现对时间敏感的过期处理。TTL 删除是最终一致的——后台清理任务按尽力而为的计划运行,过期项目可能在 TTL 时间戳之后继续可见数小时,实践中最长约 48 小时,直到清理任务将其删除。TTL 适用于存储回收和清理;不适用于会话、token、权限或实时事件触发器等安全敏感过期场景。
    由此产生两项后果:
    • **在读取时过滤。**由于项目可能在 sweeper 运行前超过 TTL,所有关心过期时间的读取路径都必须检查 TTL 属性本身——使用
      #ttl > :now
      的
      FilterExpression
      ,或在应用侧检查。不要假设 DynamoDB 返回的项目在逻辑上仍然有效。
    • **需要精确时序时配合 EventBridge Scheduler。**如果工作负载需要在到期时执行精确操作,例如撤销会话、发送提醒或释放锁,则在写入时为准确时间戳创建一次性 EventBridge Scheduler 调用,由该调用执行工作。TTL 负责最终清理,scheduler 负责时间敏感触发。Streams 随后可以将实际 TTL 删除传播给归档或审计(见集成 #7),但 Streams 不是计时机制。
    TTL 属性必须是 Unix epoch 秒值,不能是毫秒。
  13. 使用事务而非应用逻辑来保证唯一性。创建兄弟查找项目,例如
    UNIQUE#email#user@example.com
    ,并在单次
    TransactWriteItems
    调用中与实体一起写入。应用代码中的先检查后写入存在竞态窗口;事务不存在该问题。
  14. 在事务限制内工作。
    TransactWriteItems
    受到**三个硬限制——问题涉及事务限制时必须始终同时说明全部三个,不能只说其中一两个。**这些限制彼此独立,超过任意一个都会导致事务被拒绝:
    • 每笔事务 100 个项目——数组中项目总数。
    • 总 payload 4 MB——事务中所有项目的合计大小。不能遗漏此限制,也不能把它合并到 100 个项目的条目中;例如 10 个项目、每个 500 KB 的事务满足 100 个项目限制,却会违反 4 MB 限制。payload 上限准确为 4 MB——不是 16 MB(那是
      BatchGetItem
      ),不是 1 MB(那是
      Query
      /
      Scan
      分页上限),也不是 400 KB(那是最大项目大小)。
    • 单一区域——事务只在一个区域内执行,不能跨越 Global Tables 副本。如果不同区域的调用方需要参与同一事务,标准 Global Tables 就不适合该工作负载,应使用 MRSC 或重构,使每笔事务保持区域本地化。
    经常超出这些限制的操作意味着聚合边界过于粗糙,而不是需要绕过的数据库限制。导入或批量操作跨越 100 个项目边界时,应拆分为多笔事务,并显式处理部分失败恢复,例如使用 job ID、检查点进度和每个分块的幂等重试。
    **针对 100 个项目限制,应计算项目而非记录。**当一条逻辑记录需要多个 DynamoDB 项目时——例如一个联系人,加上机制 #13 的唯一性哨兵,再加一次计数器递增——
    TransactWriteItems
    数组中的每个项目都会计数。每个联系人 3 个项目意味着每笔事务大约只能容纳 30–33 个联系人,而不是 100 个。4 MB payload 限制会独立施加第二个上限;对于每个约 30 KB 的记录,4 MB 上限会先于 100 个项目上限生效。应同时针对两个限制规划分块大小,并选择较小者。
    **超过 100 个项目边界的批量导入应作为后台作业,而不是同步 API 调用。**循环调用
    TransactWriteItems
    并分页处理数万条记录,会超过任何合理的请求超时(API Gateway 30 秒、Lambda 15 分钟上限、浏览器请求超时)。应将操作建模为持久化作业:接收请求、加入作业记录、由 worker(Step Functions、SQS + Lambda、批处理作业)驱动分块执行,并使用检查点进度,使重启后能够从中断位置继续,异步报告完成情况。不要把“分块事务”视为能够组合成单次同步操作。
  15. 默认使用最终一致读取。强一致读取消耗双倍 RCU。只在需要 read-your-writes 语义的边界使用,不要将其作为全局默认。
    当调用方更新项目后立即需要最新值时,优先使用
    UpdateItem
    的
    ReturnValues=ALL_NEW
    (也可按需选择
    ALL_OLD
    、
    UPDATED_NEW
    或
    UPDATED_OLD
    ),而不是执行后续读取。写入会在响应中返回更新后的项目,不产生额外 RCU 成本,也不存在一致性问题——写入响应按其构造方式就是权威结果。
    UpdateItem
    后再执行强一致
    GetItem
    会多消耗 1 RCU,并增加一次网络往返,而所需信息原本已免费返回。这是 DynamoDB 上最省成本的 read-your-writes 形态。
  16. 将每种访问模式解析为
    GetItem
    、
    Query
    或
    BatchGetItem
    。
    Scan
    仅适用于管理操作,不适用于应用读取路径。生产访问模式如果需要 scan,说明存在建模缺口,而不是可接受的查询选择。
  17. 约束每个查询。指定排序 key 范围或过滤条件,计算预期页面大小,并使用
    LastEvaluatedKey
    分页。约束可以来自 key 结构本身——一个包含数量有界子项的父分区,例如一个订单下的订单项、一个团队下的成员,“返回该父项下全部内容”本身就是有效访问模式。真正的访问模式不是查询结果规模会随数据集无限增长的查询——例如多年累计的“某客户的所有订单”或“日志中的所有事件”——这类查询没有范围、没有页面目标,也没有终止条件。随集合增长而无界的结果集属于设计缺口,不是查询选择。
  18. 计算每种访问模式的成本。读取价格取决于 API:
    • GetItem / BatchGetItem:按返回项目计费,每项向上取整到 4 KB。最终一致(默认)= 每项每 4 KB 0.5 RCU;强一致 = 每项每 4 KB 1 RCU;事务读取 = 每项每 4 KB 2 RCU。
      BatchGetItem
      不提供逐请求折扣——每项独立计费。
    • Query:按匹配 key condition 的项目总大小计费,每 4 KB 向上取整,使用与
      GetItem
      相同的每 4 KB 费率。
      FilterExpression
      在计费之后应用——过滤掉 90% 的匹配项不会降低 90% 成本。
    • Scan:按实际检查的项目总大小计费——即从表或 GSI 读取的部分——同样在应用任何过滤之前计费。这正是
      Scan
      不适合热点路径的根本原因。
    写入价格:
    WCU = frequency × ⌈item_size / 1 KB⌉ × copies_updated
    。
    UpdateItem
    按更新前后大小的较大者计费,不是按增量,也不是只按更新后大小——即使更新使项目缩小,仍按较大的更新前大小向上取整计费。事务写入成本为 2 倍。
    ConditionalCheckFailedException
    仍消耗与同形状成功写入相同的 WCU;估算时应纳入
    conditional_fail_rate
    。
    如果某个模式无法写出这些公式,说明其定义还不够精确,无法建模。
  19. 默认使用按需容量;有证据后再切换到预置容量。按需模式消除了一类容量相关事故,是新工作负载或可变工作负载的正确默认选择。在高吞吐且模式可预测的情况下,配合自动扩缩容的预置容量可提供更低的单位成本。切换应由实测流量触发,而不是凭预期触发。

Patterns

模式

These are concrete implementation patterns that sit alongside the axioms. They are not axioms in the "correctness/operational/cost/style" sense — they are load-bearing details that a complete design needs even when no axiom explicitly requires them. Each one is worked out in full in the reference architecture; this section is the short form so the agent recognizes when it applies.
  1. Protect mutating API routes with an
    IdempotencyKeys
    table.
    Every
    POST
    ,
    PUT
    ,
    PATCH
    , or
    DELETE
    that can be retried by a client (mobile network hiccup, load-balancer retry, user double-tap) must be wrapped by idempotency middleware. Shape: a dedicated table,
    PK = <user_id>:<METHOD>:<path>:<client-supplied-uuid>
    , attributes
    status
    ,
    response_status
    ,
    response_body
    ,
    expiration
    (epoch seconds). The middleware does a conditional
    PutItem
    with
    attribute_not_exists(id)
    before the handler runs; on a conflict, it reads the cached response back and returns it. TTL is 24 hours; PITR off (the table is a cache). This is the mechanism that keeps a retried "create order" from double-charging a customer or double-decrementing inventory — the guarantee
    TransactWriteItems
    gives within a single call but not across retries.
  2. Guard mutating writes with conditional expressions. Two halves — both are mandatory guidance whenever this pattern comes up.
    Half A —
    attribute_exists(<pk>)
    on
    UpdateItem
    unless you explicitly want upsert.
    UpdateItem
    defaults to create if missing. A
    PATCH /orders/:id
    with a guessed or stale
    order_id
    silently materializes a phantom order row — no error, no log, no recovery. Add
    ConditionExpression: attribute_exists(<partition_key>)
    to every
    UpdateItem
    that operates on a row the caller is asserting exists; catch
    ConditionalCheckFailedException
    and translate to 404.
    Half B —
    attribute_not_exists(<pk>)
    on
    PutItem
    when you mean "fail on overwrite."
    PutItem
    defaults to overwrite if present. A
    POST /orders
    that should create a new order (or a new review, a new courier-assignment, a new uniqueness sentinel) will silently clobber an existing row keyed the same way without a guard. Add
    ConditionExpression: attribute_not_exists(<partition_key>)
    to every
    PutItem
    that should fail if the key already exists; catch
    ConditionalCheckFailedException
    and translate to 409 Conflict.
    Conditional writes are where invariants live in the data layer. When you recommend one half, surface the other — design reviews routinely catch the missing
    PutItem
    guard in apps that added the
    UpdateItem
    guard years earlier.
  3. TTL attributes are Unix epoch seconds. Three common format mistakes silently break expiration — enumerate all three whenever diagnosing a "TTL isn't deleting anything" report. DynamoDB TTL reads the attribute as a number and interprets it as seconds since epoch. Any of the following causes TTL to silently never fire (no error, no log — the sweeper just skips the row):
    1. Milliseconds instead of seconds.
      Date.now()
      in JavaScript returns milliseconds; written unchanged, the stamp is ~1000× too large and resolves to a year-50,000 timestamp that will never be in the past. Fix:
      Math.floor(Date.now() / 1000)
      in JS,
      int(time.time())
      in Python.
    2. ISO 8601 strings instead of numbers.
      datetime.datetime.now().isoformat()
      in Python or
      new Date().toISOString()
      in JS produces a string like
      "2026-05-13T09:45:00Z"
      . DynamoDB TTL reads strings as non-numeric and skips the row entirely.
    3. Wrong attribute name. TTL is configured per-table with a specific attribute name (
      ttl
      ,
      expiration
      ,
      expires_at
      , etc.). If the app writes
      expires_at
      but TTL is configured on
      ttl
      , or if a typo splits the attribute, TTL scans an attribute that doesn't exist and skips the row. Verify the configured attribute name in the table's TTL specification matches what the application writes, exactly.
    Validate at write time: flag any TTL value greater than
    current_time + 50 years
    as a likely milliseconds-vs-seconds bug before it reaches the table. Standardize on the attribute name across the codebase. This pairs with Mechanics #12: the "filter on read" discipline still applies because even when TTL is configured correctly, deletion is eventual.
  4. Do not persist a
    Notifications
    table for transient push/email/SMS.
    The reflex to create a table for every domain noun is a trap here. If notifications are dispatched-and-forgotten — push to APNS/FCM, transactional email via SES — the correct shape is EventBridge → SQS → NotificationLambda → delivery service. DynamoDB is reserved for state that needs to be queried later. Only create a
    Notifications
    table if you have a genuine read access pattern against it — a per-user in-app inbox, a delivery-status dashboard, a compliance audit. Otherwise you are paying DynamoDB write cost to never read the result. Per-user inbox, if needed:
    PK = user_id
    ,
    SK = <createdAt>#<notificationId>
    , TTL on older entries.
  5. Plan cascade deletes as chunked
    BatchWriteItem
    , not a sync API call.
    Deleting a parent entity in a hierarchical schema (a board, an order, a tenant, a user under GDPR) requires deleting everything keyed under it. Do it with
    BatchWriteItem
    (25 items per batch) paginated with
    Query(..., Limit=25, LastEvaluatedKey=...)
    . This is a background job, not a synchronous API response — at scale the walk is long enough to exceed a typical Lambda timeout, and partial failures must be retryable. If GDPR or regulatory deletion is a requirement of the domain, model this job explicitly as part of the design; do not assume it will fall out of the schema for free.
以下是与公理并列的具体实现模式。它们不属于“正确性/运营/成本/风格”意义上的公理,而是完整设计所需的承重细节,即使没有公理明确要求也需要具备。每个模式都在参考架构中完整展开;本节提供简短形式,帮助 Agent 识别何时适用。
  1. **使用
    IdempotencyKeys
    表保护可变的 API 路由。**每个可能被客户端重试的
    POST
    、
    PUT
    、
    PATCH
    或
    DELETE
    (例如移动网络波动、负载均衡器重试、用户双击)都必须由幂等中间件包裹。形态:使用专用表,
    PK = <user_id>:<METHOD>:<path>:<client-supplied-uuid>
    ,属性包括
    status
    、
    response_status
    、
    response_body
    、
    expiration
    (epoch 秒)。中间件在处理器执行前,使用
    attribute_not_exists(id)
    条件执行
    PutItem
    ;发生冲突时,读回缓存的响应并返回。TTL 为 24 小时;关闭 PITR(该表是缓存)。这一机制可以防止重试的“创建订单”操作重复扣款或重复扣减库存;
    TransactWriteItems
    能保证单次调用内部的一致性,却无法跨重试保证这一点。
  2. 使用条件表达式保护可变写入。两个部分——每当讨论此模式时,两部分都必须作为指导同时给出。
    部分 A——除非明确需要 upsert,否则在
    UpdateItem
    上使用
    attribute_exists(<pk>)
    。
    UpdateItem
    默认在项目不存在时创建。一个使用猜测或陈旧
    order_id
    的
    PATCH /orders/:id
    会静默生成幻影订单记录——没有错误、没有日志,也没有恢复路径。对于所有用于更新调用方明确断言已存在记录的
    UpdateItem
    ,都应添加
    ConditionExpression: attribute_exists(<partition_key>)
    ;捕获
    ConditionalCheckFailedException
    并转换为 404。
    部分 B——当
    PutItem
    的语义是“已存在则失败”时,使用
    attribute_not_exists(<pk>)
    。
    PutItem
    默认在项目已存在时覆盖。一个应创建新订单的
    POST /orders
    (或创建新评论、新配送分配、新唯一性哨兵),会静默覆盖使用相同 key 的现有记录。应向所有需要避免 key 冲突的
    PutItem
    添加
    ConditionExpression: attribute_not_exists(<partition_key>)
    ;捕获
    ConditionalCheckFailedException
    并转换为 409 Conflict。
    条件写入是数据层中不变量的落脚点。当推荐其中一个部分时,也应指出另一个部分——设计审查经常发现应用数年前加入
    UpdateItem
    保护,却遗漏了
    PutItem
    保护。
  3. **TTL 属性使用 Unix epoch 秒。三个常见格式错误会在无任何报错的情况下静默破坏过期机制——诊断“TTL 为什么不删除任何内容”时必须逐一列出。**DynamoDB TTL 将属性读取为数值,并解释为从 epoch 开始的秒数。以下任一情况都会导致 TTL 永远不触发(没有错误、没有日志——sweeper 只会跳过该行):
    1. **使用毫秒而非秒。**JavaScript 中的
      Date.now()
      返回毫秒;如果原样写入,时间戳会大约大 1000 倍,并解析为约 50,000 年的时间点,永远不会处于过去。应修复为 JS 中的
      Math.floor(Date.now() / 1000)
      ,Python 中的
      int(time.time())
      。
    2. **使用 ISO 8601 字符串而非数值。**Python 中的
      datetime.datetime.now().isoformat()
      或 JS 中的
      new Date().toISOString()
      会生成类似
      "2026-05-13T09:45:00Z"
      的字符串。DynamoDB TTL 会将字符串读取为非数值并跳过整行。
    3. **属性名错误。**TTL 按表配置,并指定具体属性名,例如
      ttl
      、
      expiration
      、
      expires_at
      。如果应用写入
      expires_at
      ,但 TTL 配置为
      ttl
      ,或者拼写错误导致属性分裂,TTL 就会扫描不存在的属性并跳过该行。必须验证表 TTL specification 中配置的属性名与应用实际写入的属性名完全一致。
    在写入时进行验证:如果 TTL 值大于
    current_time + 50 years
    ,应在值到达表之前将其标记为可能的毫秒/秒单位错误。在整个代码库中统一属性名。这与机制 #12 配套:即使 TTL 配置正确,删除仍是最终一致的,过滤读取纪律依然适用。
  4. **不要为瞬时推送、邮件或短信持久化
    Notifications
    表。**为每个领域名词创建一张表是一种本能陷阱。如果通知发出后无需再查询——推送到 APNS/FCM、通过 SES 发送事务性邮件——正确形态是 EventBridge → SQS → NotificationLambda → 投递服务。DynamoDB 只应用于之后需要被查询的状态。只有确实存在读取访问模式时,才创建
    Notifications
    表,例如按用户查询的应用内收件箱、投递状态仪表板或合规审计。否则你是在为永不读取的结果支付 DynamoDB 写入成本。如有需要,可使用按用户划分的收件箱:
    PK = user_id
    ,
    SK = <createdAt>#<notificationId>
    ,并通过 TTL 清理旧记录。
  5. **将级联删除规划为分块
    BatchWriteItem
    ,而不是同步 API 调用。**在层级 schema 中删除父实体,例如 board、order、tenant,或依据 GDPR 删除 user,都要求删除该 key 下的所有内容。应使用
    BatchWriteItem
    (每批 25 个项目),并通过
    Query(..., Limit=25, LastEvaluatedKey=...)
    分页。这应作为后台作业,而不是同步 API 响应——在规模化场景中,遍历过程足够长,可能超过典型 Lambda 超时,而且部分失败必须可重试。如果 GDPR 或监管删除是领域要求,应将此作业明确建模为设计的一部分;不要假设它会从 schema 中自动产生。

Cost estimation

成本估算

Designing and discussing the data model is the default, no-AWS path, and a user is often there to explore options — keys, GSIs, single- vs multi-table, projection tradeoffs — without yet wanting a dollar figure. Do not run the calculator reflexively on every design turn. Produce the monthly cost estimate when either is true: (a) the user asks what it costs (or for the cost report), or (b) the design is being finalized — they signal they're settling on it, taking it to a review, or otherwise want the numbers committed. Until then, stay in design discussion.
Skip the estimate entirely when no full design was produced — a single-query sizing, a hot-partition debugging thread, a pointed mechanics question. In those cases there is nothing to estimate.
When you do produce the estimate, three steps are not optional: (a) write
dynamodb_data_model.json
, (b) invoke
${SKILL_DIR}/scripts/calculate_costs.py
, (c) reference the generated
cost_report.md
in your summary. Computing numbers inline without running the calculator is never a substitute — not even for a "rough" or "realistic" figure, and not even to reconcile a headline number against a stated daily volume (for that, set
avg_rps
and let the calculator produce the expected-volume scenario — see below). The calculator enforces the pricing module that stays calibrated against live billing; doing the arithmetic by hand means the numbers drift silently and the user ships a quote they cannot defend.
The calculator is at
${SKILL_DIR}/scripts/calculate_costs.py
. The JSON schema it consumes is documented at
${SKILL_DIR}/references/cost-model-schema.md
— read that file the first time you produce a cost estimate.
The workflow:
  1. Serialize the design to
    dynamodb_data_model.json
    .
    The calculator does not reinvent the design — it reads what you already produced. The access-pattern list you built (per Mechanics #2, #18) is a near-literal translation into the JSON's
    access_patterns
    array; the schema you built (per Data modeling #3, #7, Mechanics #4, #7) translates into
    tables
    and
    gsis
    . If a field the calculator wants is missing (most commonly
    peak_rps
    on a pattern), that is a gap in the design itself — close it by asking the user or by making a defensible assumption and labeling it as one, not by writing
    0
    .
    When the user states an average or daily volume, set
    avg_rps
    on the pattern — do not hand-compute a "realistic" figure.
    The headline is peak-sustained (every pattern at
    peak_rps
    , 24/7); a user who hears a large peak-sustained monthly figure for a 15K-orders/day workload needs the expected number too. Setting
    avg_rps
    (e.g. 15,000 orders/day ÷ 86,400 s ≈ 0.17 rps average, vs a 600 rps Black-Friday peak) makes the calculator emit a second Expected Monthly Cost headline at that average rate. This is the only sanctioned way to produce a realistic-volume number — reconciling the peak headline against a daily volume with mental arithmetic is exactly the inline-arithmetic the rule above forbids.
  2. Run the calculator. Write the JSON to a workspace path of your choosing and invoke:
    bash
    python3 ${SKILL_DIR}/scripts/calculate_costs.py --model /path/to/dynamodb_data_model.json --output /path/to/cost_report.md
    The JSON is an intermediate artifact. You do not need to present it to the user unless they ask.
  3. Surface the cost report. Hand back
    cost_report.md
    — either inline (if short) or as a file pointer. Highlight the top drivers the report surfaces (the "cost patterns sorted descending" table), and call out the assumptions you baked in (RPS numbers you estimated, retention you assumed, consistency choices). The disclaimer at the top of the report is part of the artifact — do not strip it.
  4. Use the cost report to challenge the design, not just to report the number. A cost estimate is diagnostic. If one pattern dominates the bill — the classic case is a high-frequency write like a GPS ping — that is a modeling signal, not just a line item: revisit the aggregate choice (Mechanics #1), the projection shape (Mechanics #7), whether a sparse-GSI-on-transition would cut amplification (Mechanics #6), or whether the pattern belongs outside DynamoDB entirely (Integration #8). Propose the alternative and re-run the calculator on the revised model. This is the main value of having the calculator bundled: the feedback loop from "here's the design" → "here's the bill" → "here's the cheaper design" becomes a turn or two, not a separate exercise.
The calculator models request and storage cost only. It does not model Streams read/write costs, PITR, backup, DAX, or data transfer. If those line items are likely material for the workload, call that out alongside the estimate so the user knows what the number excludes. Storage is priced at the full public rate — the 25 GB free tier is not assumed (it is account-wide and often already used), so never tell the user "storage is free." Set a write pattern's
retention_days
to size its stored data; the calculator honors it. The storage figure is a bounded-retention snapshot at that
retention_days
— for a table with no TTL / "kept forever" data, real storage keeps growing past the snapshot, so do not reframe a steady-state storage number as "after N years": either state the retention window the number assumes, or note that a no-TTL table accumulates without bound.
Name the artifacts in your final response. Do not let the tool calls disappear into prose. The user reads your natural-language output, not your shell history. After you run
calculate_costs.py
, your response must name (a) the JSON file you wrote (
dynamodb_data_model.json
), (b) the script you invoked (
${SKILL_DIR}/scripts/calculate_costs.py
), and (c) the report file it produced (
cost_report.md
). A response that shows cost numbers but does not name the artifact files reads as if you computed them by hand — the workflow evidence has to be in the prose, not just the tool trace.
设计并讨论数据模型是默认的无 AWS 路径,用户通常希望探索选项——keys、GSI、单表与多表、projection 权衡——而暂时不需要金额。不要在每轮设计中都反射式运行计算器。只有满足以下任一条件时才产出月度成本估算:(a) 用户询问成本或成本报告;或 (b) 设计正在定稿——用户表示准备采用、提交评审,或希望将数字固化。在此之前,继续进行设计讨论。
如果没有产出完整设计,则完全跳过估算,例如单次查询规模估算、热点分区调试讨论或针对具体机制的提问。这些场景没有可估算的完整设计。
确实需要产出估算时,以下三个步骤不可省略:(a) 写入
dynamodb_data_model.json
;(b) 调用
${SKILL_DIR}/scripts/calculate_costs.py
;(c) 在总结中引用生成的
cost_report.md
。绝不能用内联计算代替运行计算器——即使是“粗略”或“现实”的数字也不能,甚至不能为了将一个概览数字与声明的每日流量进行核对而手动计算;对于后者,应设置
avg_rps
,让计算器生成预期流量场景——见下文。计算器使用经过实时账单校准的定价模块;手工计算意味着数字会悄悄漂移,最终交付一个无法辩护的报价。
计算器位于
${SKILL_DIR}/scripts/calculate_costs.py
。它所消费的 JSON schema 记录在
${SKILL_DIR}/references/cost-model-schema.md
——首次产出成本估算时阅读该文件。
工作流:
  1. **将设计序列化到
    dynamodb_data_model.json
    。**计算器不会重新设计——它读取你已经产出的内容。构建的访问模式列表(依据机制 #2、#18)几乎可以逐字转换为 JSON 的
    access_patterns
    数组;构建的 schema(依据数据建模 #3、#7,机制 #4、#7)转换为
    tables
    和
    gsis
    。如果计算器需要的字段缺失,最常见的是模式缺少
    peak_rps
    ,这意味着设计本身存在缺口——应向用户询问,或作出有依据的假设并明确标记,不能写成
    0
    。
    当用户给出平均量或每日流量时,在模式上设置
    avg_rps
    ,不要手动计算“现实”数值。标题数字采用持续峰值模式(每种模式都按
    peak_rps
    、全天候运行);当用户听到 15,000 个订单/天的工作负载对应一个很大的持续峰值月度数字时,还需要预期数量。设置
    avg_rps
    (例如 15,000 个订单/天 ÷ 86,400 秒 ≈ 平均 0.17 rps,而 Black Friday 峰值为 600 rps)后,计算器会额外输出一个按该平均速率计算的
    预期月度成本
    标题。这是产出符合实际流量数字的唯一许可方式——用脑内算术将峰值标题与每日流量进行核对,恰恰是上述禁止内联算术的行为。
  2. **运行计算器。**将 JSON 写入所选工作区路径,并调用:
    bash
    python3 ${SKILL_DIR}/scripts/calculate_costs.py --model /path/to/dynamodb_data_model.json --output /path/to/cost_report.md
    JSON 是中间交付物。除非用户要求,否则不必向其展示。
  3. **呈现成本报告。**交回
    cost_report.md
    ,可以内联展示(如果较短),也可以提供文件路径。突出报告呈现的主要成本驱动因素(“按成本降序排列的模式”表),并说明所采用的假设,包括估算的 RPS、保留期和一致性选择。报告顶部的免责声明是交付物的一部分——不要删除。
  4. **使用成本报告挑战设计,而不仅是报告数字。**成本估算具有诊断作用。如果某个模式主导账单,例如高频 GPS ping 写入,这属于建模信号,而不只是账单项:重新评估聚合选择(机制 #1)、projection 形态(机制 #7)、是否可通过“仅在状态转换时存在的稀疏 GSI”减少放大(机制 #6),或者该模式是否应完全移出 DynamoDB(集成 #8)。提出替代方案,并针对修改后的模型重新运行计算器。这是内置计算器的主要价值:反馈循环从“这里有设计”到“这里是账单”,再到“这里有更便宜的设计”,只需一到两个轮次,而不需要单独练习。
计算器只建模请求和存储成本。它不建模 Streams 读写成本、PITR、备份、DAX 或数据传输。如果这些项目对工作负载可能具有显著影响,应在估算旁明确指出,让用户知道数字未包含哪些内容。存储按完整公开价格计费——不假定 25 GB 免费额度(该额度属于整个账户,而且通常已被使用),因此绝不能告诉用户“存储是免费的”。为写入模式设置
retention_days
以计算存储数据量,计算器会遵循该值。存储数字是
retention_days
对应的有界保留期快照——对于没有 TTL 或“永久保留”数据的表,真实存储会持续增长并超过快照,因此不要将稳定状态存储数字重新表述为“几年之后”的结果;应说明数字所假设的保留窗口,或注明无 TTL 表会无限累积。
在最终响应中明确列出交付物。不要让工具调用消失在叙述中。用户阅读自然语言输出,而不是 shell 历史。运行
calculate_costs.py
后,响应必须明确写出 (a) 创建的 JSON 文件
dynamodb_data_model.json
、
(b)
调用的脚本
${SKILL_DIR}/scripts/calculate_costs.py
,以及 (c) 生成的报告文件
cost_report.md
。响应若展示成本数字,却不列出交付物文件,就会让人以为数字是手工计算的——工作流证据必须出现在叙述中,而不只是工具追踪中。

Live validation

实时验证

After the cost estimate, offer to run a live validation — deploy the tables and GSIs to an AWS account the user nominates and drive scaled-down traffic against them to measure real per-operation capacity and round-trip latency. Ask in one line, and run only on explicit agreement. Even in that one-line offer, name the two constraints the user needs in order to opt in responsibly: it runs only against a sandbox/testing account (never production or real user data), and cleanup is their responsibility (the skill hands back a
teardown.sh
and never auto-deletes). The full four-fact disclosure still comes before any deploy (below) — this is just so the one-line offer isn't misleadingly light. Skip for narrow questions — single-query sizing, pointed debugging, pointed mechanics — where no full design was produced and there is nothing to benchmark.
Before running, make the safety expectation explicit in your own wording (the
deploy_model.py
script also prints a warning banner, but that's a belt-and-braces backstop — the agent is responsible for setting the expectation). State the following four facts verbatim in your response; do not paraphrase them into a shorter summary:
  1. Live validation creates real DynamoDB tables, a Lambda function, and an IAM role in the AWS account the user nominates.
  2. It incurs real (small) AWS charges — single-digit cents in practice, but a real bill.
  3. The account must be a testing or sandbox account — never a production account, never an account holding real user data.
  4. The skill does not auto-teardown — at the end, the user is handed a
    teardown.sh
    script to review, and the skill will execute it on their behalf only after explicit review + intent (see step 6 below).
After stating those four facts, ask the user to explicitly name the profile, the region, and confirm the account's purpose. Only then produce the
benchmark_config.json
and invoke
deploy_model.py
. If the user hasn't named a testing account, stop and ask. Shortening this to "this creates real resources, confirm your profile" is not sufficient — enumerate the four facts because the user often doesn't know which specific resources get created or that teardown is their responsibility.
The step differs from the cost estimate in two respects that change how it must be run:
  1. Real resources, real bill. A scaled-down run costs single-digit cents in practice, but against a real account. Never run silently. Always require an explicit AWS profile, region, and caller-identity confirmation.
  2. Teardown is two-phase. Phase 1: the skill generates
    teardown.sh
    and hands it to the user. Phase 2: the skill may execute it — but only after the user has explicitly stated they reviewed the script AND explicitly directed the skill to run it. The skill never calls
    DeleteTable
    on its own initiative, and never runs
    teardown.sh
    on the basis of a bare "go" or "proceed."
Refuse to start by enumerating the four preconditions explicitly. Emit them as numbered items in your response; do not fold them into a generic "I need more information" paragraph. Surface the specific precondition that failed so the user knows what blocked the run:
  1. access_patterns
    is empty, or any pattern has
    peak_rps
    missing or
    0
    → refuse with Mechanics #2 framing: "Unknown RPS is a design gap, not a benchmark input. Pattern
    <id>
    has no declared RPS — please supply an estimate before benchmarking."
  2. boto3
    is not installed → print exactly:
    pip install boto3>=1.34
    .
  3. AWS credentials are missing or expired → print the exact remediation, e.g.
    aws sso login --profile <profile>
    for SSO, or the specific boto3 exception's suggested fix.
  4. Caller identity's account alias or ARN contains
    prod
    ,
    production
    ,
    prd
    , or
    live
    → refuse and tell the user to supply a sandbox/testing profile.
Check all four before writing
benchmark_config.json
or invoking
deploy_model.py
. A generic "I need more info" paraphrase does not satisfy the contract — enumerate the specific precondition that failed.
Workflow, once the user agrees:
  1. Reuse
    dynamodb_data_model.json
    from the cost estimate. Do not re-serialize.
    Before deploying, confirm every numeric/binary key declares its type. The deploy builds each table's
    AttributeDefinitions
    by looking up every key attribute (table + GSI partition/sort keys) in
    entities[].attributes[]
    and
    attribute_definitions
    , defaulting any it can't find to
    S
    . A numeric epoch sort key or numeric id left undeclared deploys as a string and then rejects real writes with
    ValidationException
    . Per Data modeling #11, ensure each such key carries
    "type": "N"
    (or
    "B"
    ) in the model before this step — and don't trust a clean seed as proof, since the failure can surface only on live writes. If you want belt-and-suspenders,
    DescribeTable
    after deploy and confirm the key's
    AttributeType
    .
    Scope the run when the design has many patterns — warn, estimate, and get confirmation. The benchmark drives patterns serially (each pattern runs its own warmup + measurement window one at a time), so wall-clock grows with pattern count: roughly
    table_settle + Σ_patterns (warmup + duration)
    . At standard windows (~10s warmup + ~60–90s duration) a 6-pattern design is a few minutes, but a 20+ pattern design is 25–45+ minutes and gets split across several sequential Lambda invocations. Before launching a run on a design with more than ~8 patterns: state a rough wall-clock estimate and get explicit confirmation ("This will benchmark all 24 patterns and take ~35–45 minutes — proceed, or would you rather test a focused subset?"). Don't silently kick off a 40-minute run.
    Offer to load-test only the critical patterns, and choose them with the user. A full live run rarely needs every pattern — most are cheap structured lookups whose per-op cost the calculator already nails. Offer to benchmark a focused subset and collaborate on which ones, steering toward the patterns where a live measurement actually buys something:
    • Highest-throughput / firehose writes — the ones whose RPS dominates the bill or stresses capacity (e.g. a location-ping write).
    • The transactional / multi-item core —
      TransactWriteItems
      , multi-table writes, the checkout path (transaction 2× cost, Mechanics #18, is worth seeing live).
    • Novel or risky key shapes — anything with a fan-out read (N parallel Queries), a hot-key risk, or a GSI you're unsure about; skip the boring
      GetItem
      -by-id patterns.
    • Cost-dominant patterns — whatever the cost report flagged as the top drivers.
    To run a subset, benchmark a trimmed copy of the model: keep all
    tables
    (so the schema deploys and seeds correctly) but prune
    access_patterns
    to the chosen few. The cost estimate is unaffected — it always runs on the full model against declared peak; subsetting only narrows the live driven traffic, not the design or its costing. Name which patterns you're testing and which you're skipping (and why) so the user sees the coverage tradeoff.
  2. Write
    benchmark_config.json
    — profile and region are the only fields you must set (
    resource_prefix
    is auto-generated by the deploy when omitted; everything else has a mode-driven default). Read
    ${SKILL_DIR}/references/performance-model-schema.md
    the first time. Ask the user which mode to run BEFORE writing the file — do not pick a default silently. For a one-shot live validation, the choice is between the two unit-cost modes below; the third mode,
    "representative"
    , is the iterative design loop's mode (hot-key skew at bounded scale) and is described under Iterative design loop — offer it only when the user wants to probe hot-partition/throttle risk, not for a plain cost-validation run.
    State the driven throughput honestly, and offer to drive harder. This is the most important thing to get right about live validation. By default
    quick
    /
    standard
    drive only a small fraction of declared peak (
    scale_factor
    ~0.01, floored at
    min_rps_per_pattern
    ) — for a design whose peaks are modest this floors to ~1 request/second per pattern. That is a unit-cost sample: it validates per-operation capacity (cost) and unloaded latency, and it is cheap (pennies, ~1–2 min/pattern). It does NOT exercise load — at ~1 rps, zero throttles is mathematically guaranteed (on-demand baseline is ~2,000 WCU / ~4,000 RCU) and the latencies are best-case. So when you offer the run, say which it is, and offer the alternative:
    • Unit-cost sample (default) — validates cost + unloaded latency. Cheap. Does not test load/throttling.
    • Drive at/near declared peak — a real load test. Set
      scale_factor
      (fraction of declared peak, e.g.
      1.0
      for full peak) or
      max_rps_per_pattern
      in
      benchmark_config.json
      ; both are honored. Bounded by one in-region Lambda's ceiling (~1,500–2,000 rps/pattern) and gated by
      cost_guardrail_usd
      +
      --allow-spend
      (so an expensive run can't fire silently). Caveat: a freshly-created on-demand table starts at ~2,000 WCU / ~4,000 RCU baseline and adapts upward with a lag, so a high rate immediately after deploy measures cold-start adaptive-capacity warm-up, not steady state — the run already has
      ramp_seconds
      +
      table_settle_seconds
      to absorb some of this; for a clean high-rps read use a longer
      duration_seconds
      .
    • representative
      mode
      — the loop's hot-key/throttle-risk mode (zipf skew at bounded scale); use it when hot-partition behaviour is the question (see Iterative design loop).
    The extrapolated monthly cost is identical across
    quick
    /
    standard
    and at any throughput — per-op CU is deterministic and the cost extrapolation always uses declared peak, so a higher-throughput run buys you latency-under-load and throttle signals, not a different cost number. Always tell the user the per-pattern rate you will drive before you run. Emit the mode options as a literal question and wait for the user to pick:
    Which mode should I run? Pick one:
    • "quick"
      — ≈1 minute per pattern window, a smoke test that confirms the design deploys and runs. Percentiles are less stable. A unit-cost sample (drives ~1 rps/pattern for a modest-peak design): validates cost + unloaded latency, not load.
    • "standard"
      (default) — full-length windows, stable p50/p95/p99, trustworthy extrapolated monthly cost. Still a unit-cost sample by default (same ~1 rps/pattern) — the cost number is identical to
      quick
      ; you get tighter latency percentiles, not a load test. To make it a load test, tell me to drive at/near your declared peak. (There is also
      "representative"
      — hot-key/throttle-risk mode for the iterative design loop. Mention it only if hot-partition behavior is the question; see Iterative design loop.)
    Write the user's chosen value into
    benchmark_config.json
    's
    mode
    field. If the user skips the question and just says "go," default to
    "standard"
    and name that default in your reply ("I'll run
    standard
    mode since you didn't specify — interrupt if you meant
    quick
    ").
  3. Deploy:
    python3 "$DDB_SKILL_DIR/scripts/deploy_model.py" --model … --config … --manifest-out created_resources.json --yes-deploy
    . Prints caller identity for confirmation, aborts on prod markers, prefixes every resource with
    ddb-skill-bench-<date>-<uuid8>-
    (generated for you if you didn't set
    resource_prefix
    ), tags it with the run id. On an interactive terminal it waits a few seconds for a Ctrl-C abort after printing the account; in an agent/CI run it proceeds immediately on the
    --yes-deploy
    consent.
  4. Benchmark:
    python3 "$DDB_SKILL_DIR/scripts/benchmark_model.py" --model … --config … --manifest created_resources.json --raw-out perf_raw.jsonl --summary-out perf_summary.json
    . Handles cold-start discipline automatically (see below).
    Execution discipline — run it in the foreground and wait. A
    representative
    run drives many minutes of traffic (≈ settle + per-pattern warmup + per-pattern duration across every pattern — often 15+ minutes for a multi-pattern design). Run the benchmark as a single blocking call and wait for it to finish — do NOT background it and hand control back to the user. A backgrounded run can be killed when the surrounding turn/session ends, leaving the previous run's
    perf_summary.json
    in place — which is exactly how a stale summary gets read as if it were fresh. If the run risks exceeding your tool's wall-clock limit, raise the tool timeout — do not shrink the windows or background the job. You do not need to hand-size
    duration_seconds
    to fit a budget: the heavy work runs inside the Lambda (≤900 s server-side) and
    benchmark_model.py
    already splits the load across sequential invocations (
    _compute_split
    ). When it finishes, note the UTC time you launched it; you'll check the summary's
    benchmark_completed_at
    against it in step 5.
  5. Report:
    python3 "$DDB_SKILL_DIR/scripts/generate_perf_report.py" --model … --summary perf_summary.json --output performance_report.md
    .
    Verify the summary is fresh AND matches the experiment you described before you interpret it (Operating discipline, applied).
    perf_summary.json
    records
    benchmark_completed_at
    (UTC) and the
    config
    the run actually used. Before generating or trusting the report, confirm (a)
    benchmark_completed_at
    is newer than the launch time you noted in step 4, and (b) the summary's
    config
    (
    mode
    ,
    provisioned_capacity
    ,
    duration_seconds
    ) matches what you set for this run. If either fails — or the field is absent — the benchmark did not complete and you are looking at a stale file: do not interpret it, re-run. The report's Deployment block surfaces
    benchmark_completed_at
    and the run window so the user can see the same thing. The tell that you're reading stale data: benchmark numbers that are byte-identical to a prior run you expected to differ.
    Run what you said you'd run — verify config-match, not just freshness. If you told the user this run would change something (raise the provisioned ceiling to isolate skew, pin a subset of patterns, drive a higher rps, switch modes), then read the actual
    config
    block in the fresh
    perf_summary.json
    and confirm it reflects that change before you report. A run that is fresh but used the old config is not the experiment you promised — interpreting it answers a different question than the one you set up. If you find you described an experiment you did not actually configure, say so plainly and either re-run with the correct config or, if the run is physically incapable of producing the signal regardless (e.g. a unit-cost/floor run at ~1 rps cannot drive any partition key hot no matter how you set capacity, so it can never isolate skew — only representative mode or driving at/near declared peak can; raising a provisioned ceiling on a floor run changes nothing — Mechanics #3, and Unit-cost / floor runs below), state that instead of quietly substituting a same-config re-run. Never present a re-run as testing a variable you did not change. The catch in practice: the user notices two "different" runs producing near-identical numbers — get ahead of it by checking the config yourself.
    ⚠ DO NOT run
    rm
    ,
    rm -f
    ,
    ls
    ,
    find
    ,
    aws dynamodb delete-table
    , or any other ad-hoc delete command when the user asks to "clean up," "tear down," "remove," or "get rid of" the benchmark resources.
    "Teardown" in this skill means invoking
    generate_teardown.py
    to produce
    teardown.sh
    , never running a shell command yourself to delete local files or AWS resources. The teardown protocol is step 6, below. Local artifact files (
    performance_report.md
    ,
    cost_report.md
    ,
    dynamodb_data_model.json
    , etc.) are the user's to keep or delete — the skill never cleans them up.
  6. Teardown — generate first, execute only after explicit review + intent. When the user asks to tear down, clean up, or remove benchmark resources:
    a. Generate the script with
    python3 ${SKILL_DIR}/scripts/generate_teardown.py --manifest created_resources.json --out teardown.sh
    . This writes
    teardown.sh
    . It does not delete anything.
    b. Surface the script path, the review command, and the execute command in your reply. Verbatim form:
    • Review:
      cat teardown.sh
      (or open in an editor) — the script deletes DynamoDB tables, a Lambda function, and an IAM role under the
      ddb-skill-bench-
      prefix.
    • Execute:
      bash teardown.sh --confirm
      — the manifest's account, region, and prefix are baked into the script at generation time, and it re-checks caller identity + prefix before deleting anything. (Pass
      --dry-run
      instead of
      --confirm
      to preview deletions; add
      --delete-logs
      to also remove the Lambda's CloudWatch log group — that log deletion is irreversible.)
    c. Do not conflate "teardown" with "local cleanup." Never run
    rm
    ,
    rm -f performance_report.md
    ,
    ls
    , or
    find
    thinking the task is removing local files. Only
    teardown.sh
    deletes AWS resources; local files are not the skill's concern.
    d. The skill may execute
    teardown.sh
    on the user's behalf, but only under this exact two-part condition:
    • The user's message must state they reviewed the script (examples: "I read it," "I reviewed teardown.sh," "I checked the contents," "looked it over"). Inferring review from context does not count; it must be stated.
    • The user's message must state the intent to run it (examples: "run it," "execute it," "go ahead and tear down," "proceed with the teardown").
    e. When the user asks the skill to run teardown without stating review, the skill must not execute. Instead, surface the exact review command (
    cat teardown.sh
    ) and ask the user to confirm they reviewed it. One required response shape:
    Before I run
    teardown.sh
    , please confirm you've reviewed the script. Run
    cat <PATH>/teardown.sh
    — it deletes the DynamoDB tables, Lambda function, and IAM role created under the
    ddb-skill-bench-
    prefix. Once you've read it and want me to execute, reply with something like "I reviewed it, go ahead" and I'll run
    bash teardown.sh --confirm
    .
    f. When the user asks the skill to skip the review ("don't worry about it," "just run it without showing me," "trust it"), refuse. Destructive actions require the user's attested review. Surface the refusal and re-offer the review command.
    g. When the conditions in (d) are met, the skill may run
    bash teardown.sh --confirm
    , surface the full stdout/stderr, and report which resources were deleted.
Operations execute as declared, never substituted. When the design says
BatchWriteItem
, the benchmark issues
BatchWriteItem
in 25-item batches. When it says
TransactWriteItems
, the benchmark issues
TransactWriteItems
with the declared item set. Single-item substitutes (a loop of
PutItem
for
BatchWriteItem
, or individual
PutItem
calls for
TransactWriteItems
) are not used — they measure a different design. Transaction 2× write cost (Mechanics #18), batch partial-failure semantics, and transaction bounds (Mechanics #14) are exactly what live validation observes; substituting them defeats the purpose. If a user asks how the benchmark exercises batch or transactional patterns, state plainly that the declared operation runs as-declared and name the specific bounds the benchmark will observe.
Cold-start discipline. DynamoDB latency and throughput are not stable at t=0 on a new table; numbers collected without discipline here will misleadingly look worse than steady state. Three classes of bias must be removed before measurements are trustworthy:
  • Fresh-table capacity. On-demand tables start at baseline capacity (~2,000 WCU, ~4,000 RCU); traffic above baseline in the first minutes can throttle until adaptive capacity scales. After
    table_exists
    returns, wait at least
    table_settle_seconds
    (default 30) before any measurement call.
  • Partition priming and client warmup. Seed writes warm the partitions the benchmark will later hit, and a fresh boto3 client carries a TLS handshake + credential refresh on its first call (tens to hundreds of ms). The benchmark pauses briefly between seed and measurement, and issues one
    DescribeTable
    per worker thread, so those one-time costs don't show up in the percentiles.
  • Per-pattern warmup window. Every pattern runs a
    warmup_seconds
    window (default 10) ahead of its measurement window. Warmup calls are recorded with
    phase: "warmup"
    and excluded from latency percentiles and the cost extrapolation. The perf report surfaces warmup p99 separately — if it's materially higher than steady-state p99, that itself characterizes cold-deploy behavior and is worth calling out.
What is and is not measured:
  • Measured: per-op consumed RCU/WCU from
    ReturnConsumedCapacity
    ; round-trip latency p50/p95/p99 (warmup and steady-state reported separately); throttle counts; GSI write amplification; even-distribution signal per pattern; observation-based extrapolated monthly cost.
  • Not measured: TTL sweep cadence, stream consumer latency and cost, autoscaling under sustained burst, cross-region replication lag, long-tail traffic shapes beyond the measurement window, and any non-DDB service in the design (Lambda, API Gateway, SQS, EventBridge Pipes, OpenSearch, etc.). The perf report lists these under "Designed, not benchmarked" so the user sees plainly which parts of their architecture the numbers cover.
The report format is documented in
${SKILL_DIR}/references/performance-report-format.md
. It mirrors
cost_report.md
's disclaimer-first, table-driven style and prints both observed and calculator-expected numbers per pattern so the user sees where their inputs (RPS, item size, consistency) diverged from reality.
Reflect on the findings and offer an iteration loop. After the report is written, read it as a diagnostic — not a final score — and ask: is there an alternative design that the measurements argue for? For each finding, classify it:
  • Input-accuracy finding — the observation diverged from the calculator because a declared input (RPS, item size, consistency,
    conditional_fail_rate
    ) was off. The design itself is fine; the estimate needs updated inputs. Propose updating the JSON and re-running the calculator only (cheap, no redeploy).
  • Design finding — the observation reveals a structural issue that persists regardless of input accuracy. Examples and the axioms that govern them:
    • Persistent throttles that scale with partition-key skew → Mechanics #3 (per-partition ceilings, write-shard the hot key). Before attributing throttles to a hot partition, run the skew-vs-starvation gate — both parts must pass: (1) Differential test — the hot partition's p99 must be materially above the cold partitions' p99 (the report's ~1.8× rule). If hot p99 ≈ cold p99, every partition throttled uniformly: that is capacity starvation, not key skew — say so, and do not attribute it to the partition key. (2) Uniform-baseline test — using the per-op observed CU the report already prints (not hand-derived item counts), check whether uniform keys at the driven RPS would already exceed the provisioned ceiling; if they would, the run cannot isolate skew — recommend re-running with capacity set above uniform demand so only a genuinely hot partition throttles. Only once both pass is "hot partition at its ceiling" a supported verdict. Note also: write-sharding a mutable partition key (e.g. a status that changes each transition) spreads the Mechanics #3 load but the key still mutates on every write, so the Mechanics #8 (mutable GSI key) deviation persists and must stay recorded per Artifact #5 — eliminating the write amplification needs a separate single-purpose table, not a sharded suffix.
    • One pattern dominating the bill → Mechanics #1 (aggregate tightness), Mechanics #7 (projection), Integration #8 (move off DDB to Timestream/S3/OpenSearch/Redshift/SageMaker Lakehouse when the workload is analytical, high-frequency telemetry, or full-text).
    • GSI write amplification higher than the projection implies → Mechanics #6 (sparse GSI when a mutable key isn't always present), Mechanics #7 (downgrade ALL → INCLUDE/KEYS_ONLY), Mechanics #8 (mutable attribute as GSI key doubles the write).
    • Strong read cost observed where RYW semantics weren't actually required → Mechanics #15 (default to eventual), Patterns around
      ReturnValues=ALL_NEW
      to avoid the post-write re-read.
    • Unbounded query hitting the 900 KB page cap repeatedly → Mechanics #17 (constrain every query; require a time window or max page).
    • Cold-start p99 materially elevated at on-demand baseline → a capacity-mode or pre-warming decision (Mechanics #19), not usually a schema change.
When at least one design finding is meaningful — meaning the proposed change would plausibly reduce cost by >10%, remove a throttle source, or close an axiom deviation — tell the user:
  1. Which findings point to design changes vs. which point to input accuracy.
  2. For each design finding, the axiom it traces to AND a literal JSON diff on
    dynamodb_data_model.json
    — not a verbal description. Emit the diff as a fenced code block showing the specific fields that change (new GSI object, dropped GSI, changed
    projection.type
    , sharded PK with hash suffix, pattern moved out of the
    access_patterns
    array entirely). A verbal suggestion like "consider downgrading the projection" does not satisfy this — the user has to see the exact change so they can accept or reject it. When the diff changes a GSI's key (a sharded partition key is the common case), label it as an additive-GSI migration, not a code-only change, per Fact #9 — the new index is created alongside, populated, cut over, then the old one dropped. Example shape:
    diff
    - { "name": "OrdersByCustomer", "projection": { "type": "ALL" } }
    + { "name": "OrdersByCustomer", "projection": { "type": "INCLUDE", "non_key_attributes": ["status", "total"] } }
  3. The expected effect on the top cost drivers and throttle sources.
Your spoken verdict inherits the report — never exceed it. The chat summary you give the user must not claim an outcome the report did not classify. Concretely:
  • If the report emitted no
    key_skew_patterns
    finding
    , do not say the design "throttles" or "hot-partitions" — the supported statement is the report's own ("no hot-partition distress observed under skew" / "not validated by this run").
  • Restate the report's hedge; do not upgrade it. "Throttles appeared under a deliberately low ceiling" is not "the GSI partition is hot" (see the skew-vs-starvation gate above).
  • Attribute every number to exactly the claim it supports. A low provisioned ceiling that throttles proves the table was under-provisioned for the driven load, not that any single partition key is hot.
  • Worked contrast — same data, two verdicts: ❌ "Confirmed: the OrdersByStatus GSI throttles hard under load." ✅ "Under a 50-WCU ceiling, writes to the Orders table throttled — but hot-partition p99 ≈ cold-partition p99, so this is uniform capacity starvation, not key skew. The run did not isolate a hot-partition effect; to test that, re-run with capacity above uniform demand."
  • Unit-cost / floor runs — do not narrate them as load tests. When the run was a unit-cost sample —
    quick
    /
    standard
    mode, or any run where every pattern's driven
    bench_rps
    equals the
    min_rps_per_pattern
    floor (it shows as a uniform
    bench_rps
    , typically ~1, in
    perf_summary.json
    , and the report's disclaimer says "UNIT-COST sample, not a load test") — you MUST NOT present "0 throttles" or the latency percentiles as evidence the design withstands load. At ≤ a few rps, zero throttles is mathematically guaranteed (on-demand baseline ~2,000 WCU / ~4,000 RCU) and the latencies are unloaded best-case. State what the run actually validated — per-op capacity (cost) and unloaded latency — and that throttling/ceilings were "not exercised — see the report's Not-validated list." If you reassure the user that throttling isn't a risk, give the structural reason, not the zero count: e.g. a high-cardinality partition key (
    user_id
    across many users) spreads load so no single partition gets hot (Mechanics #3). To actually exercise throttling, drive at/near declared peak or use representative mode.
    • Worked contrast — a ~1 rps habit-tracker run: ❌ "Validated against live DynamoDB — 0 throttles, reads <8 ms, writes <50 ms. Ready for review." ✅ "This validated per-op cost (within 2% of the model) and unloaded latency (reads <8 ms). It ran at ~1 rps, so it did not test throttling — but throttling isn't a concern for this design anyway: the partition key is
      user_id
      , so load spreads across users and no single partition gets hot. To prove peak behaviour, we'd re-run driving your declared peak rps."
  • Reconcile any number the user quotes back against your own latest artifact — do not just agree. When the user restates a figure ("$189/mo is fine," "so it's ~600 WCU on that partition," "the 1,884 throttles") before you respond, check it against the number actually in your current
    cost_report.md
    /
    perf_summary.json
    /
    performance_report.md
    — after confirming that artifact is itself fresh per step 5, since a number matching a stale summary is not a real reconciliation. If it doesn't match — even when the user sounds confident, and especially when accepting it would be the agreeable move — correct it explicitly and cite the artifact: "Quick correction: the report says $835/mo expected, not $189 — the $189 was an earlier figure. Still want to proceed?" Silently accepting a wrong number the user will then carry into a budget deck or a design review is a worse failure than a moment of friction. This applies to costs, capacity math, throttle/latency counts, and item-size assumptions alike. A user-supplied number is an input to verify, not a verdict to ratify.
  • Do not vouch for a claim the run did not establish. Your spoken verdict may assert only what the artifact in front of you actually shows. If a number you're about to state came from your own head rather than the report (a hand-extrapolated provisioned/reserved cost, a "scales linearly so 25k rps is fine" leap from a 2k-rps run, a per-partition figure the benchmark never isolated), label it as reasoning, not measurement, and give the user the structural argument behind it — never dress an estimate as a validated result. "The calculator only models on-demand; the reserved figure is my hand-calc for a provisioned alternative (reserved capacity applies to provisioned only — Fact #7) from published rates — confirm it against the AWS calculator before the deck" is honest. Presenting that hand-calc as "validated" is not.
Then ask the user whether they want to iterate: apply one or more changes and re-evaluate. Offer three levels of re-evaluation and let the user pick. Emit the three options as three literal bulleted items — do not collapse them into prose, do not drop any of the three, and do not re-order them. The user picks one by name.
  • Calculator only — update the JSON, re-run
    calculate_costs.py
    , show the new
    cost_report.md
    and a side-by-side delta. Cheap, no AWS calls, no teardown. This is the default and should be the first offer.
  • Calculator + live validation — do the above, then offer a second live run against the revised design. Requires the user to re-consent to AWS deployment and accept a second teardown script; the prior teardown should be run first unless the change is additive-only.
  • No changes — the user may reasonably decide the findings are acceptable, the cost of change outweighs the savings, or the proposed alternatives don't fit a constraint the skill doesn't know about. Record the decision as a "deviation" per Artifact #5 with the user's stated reason.
Do not iterate silently, do not iterate unboundedly. Each iteration is explicit and opt-in; if the user declines or the report surfaces no meaningful findings, stop and hand back the final artifacts.
完成成本估算后,主动提供实时验证选项——将表和 GSI 部署到用户指定的 AWS 账户,并驱动缩小规模的流量,以测量真实的逐操作容量和往返延迟。用一行询问,并且只在用户明确同意后运行。即使在这行提议中,也要说明用户负责任地选择加入所需了解的两个限制:它只针对 sandbox/testing account 运行(绝不针对生产环境或真实用户数据),并且清理由用户负责(skill 会交回
teardown.sh
,绝不会自动删除)。完整四项事实披露仍须在任何部署之前给出——这里只是避免一行提议造成不切实际的轻描淡写。对于范围过窄的问题,例如单次查询规模估算、针对性调试或针对性机制讨论——如果没有完整设计,也就没有可基准测试的内容,应跳过。
运行前,使用自己的措辞明确说明安全预期(
deploy_model.py
脚本也会打印警告横幅,但这只是额外保险;Agent 有责任设定预期)。在响应中逐字说明以下四项事实;不要改写成更短的摘要:
  1. 实时验证会在用户指定的 AWS 账户中创建真实的 DynamoDB 表、Lambda function 和 IAM role。
  2. 它会产生真实的(少量)AWS 费用——实践中通常是几分钱,但会进入真实账单。
  3. 该账户必须是 testing 或 sandbox account——绝不能是生产账户,也不能包含真实用户数据。
  4. 该 skill 不会自动执行 teardown——结束时会将
    teardown.sh
    script 交给用户审查,并且只有在用户明确完成审查并表达执行意图后,才会代其执行(见下文步骤 6)。
说明这四项事实后,要求用户明确指定 profile、region,并确认账户用途。只有在此之后,才能生成
benchmark_config.json
并调用
deploy_model.py
。如果用户没有指定 testing account,则停止并询问。将这些内容缩短为“这会创建真实资源,请确认你的 profile”并不充分——必须逐项列出四个事实,因为用户通常不知道具体会创建哪些资源,也不知道清理由他们负责。
该步骤与成本估算有两个不同之处,会改变执行方式:
  1. **真实资源、真实账单。**缩小规模的运行实践中通常花费几分钱,但发生在真实账户中。绝不能静默运行。始终要求用户明确提供 AWS profile、region 和调用方身份确认。
  2. Teardown 分为两个阶段。阶段 1:skill 生成
    teardown.sh
    并交给用户。阶段 2:skill 可以执行它——但只能在用户明确表示已经审查脚本
    并
    明确指示 skill 运行之后。skill 绝不会主动调用
    DeleteTable
    ,也绝不会仅凭一句“继续”或“执行”就运行
    teardown.sh
    。
**拒绝启动前,必须明确枚举四项前置条件。**在响应中以编号项目输出;不要将它们折叠进笼统的“我需要更多信息”段落。必须指出具体哪项前置条件失败,让用户知道是什么阻止了运行:
  1. access_patterns
    为空,或任一模式缺少
    peak_rps
    或其值为
    0
    → 使用机制 #2 的措辞拒绝:“Unknown RPS is a design gap, not a benchmark input. Pattern
    <id>
    has no declared RPS — please supply an estimate before benchmarking.”
  2. 未安装
    boto3
    → 准确输出:
    pip install boto3>=1.34
    。
  3. AWS 凭据缺失或过期 → 输出确切的修复方式,例如 SSO 使用
    aws sso login --profile <profile>
    ,或采用对应 boto3 异常所建议的修复方法。
  4. 调用方身份的 account alias 或 ARN 包含
    prod
    、
    production
    、
    prd
    或
    live
    → 拒绝,并要求用户提供 sandbox/testing profile。
在写入
benchmark_config.json
或调用
deploy_model.py
之前检查全部四项。笼统地说“我需要更多信息”不满足该契约——必须枚举具体失败的前置条件。
工作流,在用户同意后:
  1. 复用成本估算阶段的
    dynamodb_data_model.json
    。不要重新序列化。
    **部署前,确认每个数值或 binary key 都声明了类型。**部署脚本会从
    entities[].attributes[]
    和
    attribute_definitions
    中查找每个 key 属性(表与 GSI 的分区/排序 key),以构建每张表的
    AttributeDefinitions
    ;找不到时默认为
    S
    。未声明的数值 epoch 排序 key 或数字 ID 会以字符串部署,随后真实写入会因
    ValidationException
    被拒绝。依据数据建模 #11,确保每个此类 key 在模型中带有
    "type": "N"
    (或
    "B"
    )——不要把 seed 数据成功当作证明,因为故障可能只在实时写入时出现。如果希望增加保险,可在部署后调用
    DescribeTable
    ,确认 key 的
    AttributeType
    。
    设计包含许多模式时,应限制运行范围——给出警告、估算并获得确认。基准测试会串行驱动模式(每种模式依次执行各自的预热和测量窗口),因此墙钟时间随模式数量增长,大致为
    table_settle + Σ_patterns (warmup + duration)
    。在标准窗口下(约 10 秒预热 + 60–90 秒持续时间),6 个模式的设计需要数分钟,但 20 个以上模式的设计需要 25–45 分钟以上,并且会拆分到多个顺序执行的 Lambda 调用。启动包含约 8 个以上模式的设计前,应说明粗略墙钟时间估算并获得明确确认(“这将基准测试全部 24 个模式,预计需要约 35–45 分钟——继续,还是只测试一个聚焦子集?”)。不要静默启动长达 40 分钟的运行。
    **主动提供只对关键模式进行负载测试的选项,并与用户共同选择。**完整的实时运行很少需要测试每种模式——大多数是廉价结构化查找,其逐操作成本计算器已经能准确得出。提供基准测试聚焦子集的选项,并协作确定哪些模式值得测试,重点关注实时测量确实能提供价值的模式:
    • 吞吐量最高或写入洪流最大的模式——其 RPS 主导账单或对容量构成压力,例如位置 ping 写入。
    • 事务或多项目核心路径——
      TransactWriteItems
      、多表写入、结账路径;事务 2 倍成本(机制 #18)值得进行实时观察。
    • 新颖或高风险 key 形态——任何扇出读取(并行执行 N 个 Query)、热点 key 风险或不确定的 GSI;可跳过普通的按 ID
      GetItem
      模式。
    • 成本主导模式——成本报告标记出的主要成本驱动因素。
    如需测试子集,应基准测试模型的精简副本:保留所有
    tables
    (确保 schema 正确部署和填充),但将
    access_patterns
    缩减为选定的几个。成本估算不受影响——它始终基于完整模型和声明的峰值运行;缩减只会缩小实时驱动的流量范围,而不会改变设计或成本计算。明确说明正在测试和跳过哪些模式,以及跳过原因。
  2. 写入
    benchmark_config.json
    ——必须设置的字段只有 profile 和 region(省略
    resource_prefix
    时,部署脚本会自动生成;其他字段都有随模式变化的默认值)。首次运行时阅读
    ${SKILL_DIR}/references/performance-model-schema.md
    。**在写入文件之前询问用户要运行哪种模式——不要静默选择默认值。**对于一次性实时验证,需要在下面两种单位成本模式之间选择;第三种模式
    "representative"
    用于迭代设计循环(有限规模下的热点 key 偏斜),详见迭代设计循环——只有当用户希望探测热点分区或限流风险时才提供。
    如实说明所驱动的吞吐量,并主动提供更高负载选项。这是实时验证中最重要的一点。默认情况下,
    quick
    /
    standard
    只驱动声明峰值的一小部分(
    scale_factor
    约为 0.01,并以
    min_rps_per_pattern
    为下限)——对于峰值适中的设计,这会落在
    每个模式约 1 次请求/秒
    。这是单位成本样本:用于验证逐操作容量(成本)和无负载延迟,成本很低(几分钱,约每个模式 1–2 分钟)。它不施加负载——在约 1 rps 下,零限流在数学上必然成立(按需基线约为 2,000 WCU/约 4,000 RCU),延迟也是最佳情况。因此主动提供运行时,应说明所选模式,并提供另一种选择:
    • 单位成本样本(默认)——验证成本和无负载延迟。成本低,不测试负载或限流。
    • 按声明峰值或接近峰值驱动——真实负载测试。设置
      scale_factor
      (声明峰值的比例,例如完整峰值为
      1.0
      )或
      max_rps_per_pattern
      ;两者都会受到尊重。单个区域内 Lambda 的上限约为每个模式 1,500–2,000 rps,并受
      cost_guardrail_usd
      +
      --allow-spend
      限制(防止昂贵运行静默触发)。**注意:**新建的按需表从约 2,000 WCU/约 4,000 RCU 基线开始,并存在适应扩容延迟,因此部署后立即施加大流量,测到的是冷启动适应容量预热,而不是稳定状态;运行已包含
      ramp_seconds
      +
      table_settle_seconds
      来吸收部分影响;如需干净的高 RPS 读取,应使用更长的
      duration_seconds
      。
    • representative
      模式
      ——循环使用的热点 key 或限流风险模式(采用 zipf 偏斜和有限集合规模);当问题涉及热点分区行为时使用(见迭代设计循环)。
    无论采用
    quick
    /
    standard
    还是以何种吞吐量运行,推算的月度成本完全相同——逐操作 CU 是确定性的,成本推算始终基于声明的峰值。因此,更高吞吐量的运行买到的是负载下延迟和限流信号,而不是不同的成本数字。**运行前始终告诉用户将为每种模式驱动何种速率。**以固定格式提出模式选项并等待用户选择:
    应运行哪种模式?请选择一项:
    • "quick"
      ——每个模式窗口约 1 分钟,用于确认设计可部署并运行的冒烟测试。百分位稳定性较低。属于单位成本样本(对于峰值适中的设计,驱动约 1 rps/模式):验证成本和无负载延迟,不测试负载。
    • "standard"
      (默认)——完整长度窗口,提供稳定的 p50/p95/p99 和可信的月度成本推算。默认仍属于单位成本样本(同样约 1 rps/模式);成本数字与
      quick
      完全相同,但延迟百分位更稳定。若要执行负载测试,请告诉我按声明峰值或接近峰值驱动。
    • (另有
      "representative"
      ——用于迭代设计循环的热点 key 或限流风险模式。只有当热点分区行为是当前问题时才提及;见迭代设计循环。)
    将用户选择的值写入
    benchmark_config.json
    的
    mode
    字段。如果用户跳过问题并只说“继续”,默认使用
    "standard"
    ,并在回复中说明该默认选择(“你没有指定,因此我将运行
    standard
    模式——如果你原意是
    quick
    ,请随时打断”)。
  3. 部署:
    python3 "$DDB_SKILL_DIR/scripts/deploy_model.py" --model … --config … --manifest-out created_resources.json --yes-deploy
    。脚本会打印调用方身份供确认,遇到生产环境标记时中止,为所有资源添加
    ddb-skill-bench-<date>-<uuid8>-
    前缀(未设置
    resource_prefix
    时自动生成),并使用 run id 打标签。在交互式终端中,打印账户信息后会等待数秒,允许用户按 Ctrl-C 中止;在 Agent/CI 运行中,提供
    --yes-deploy
    授权后会立即继续。
  4. 基准测试:
    python3 "$DDB_SKILL_DIR/scripts/benchmark_model.py" --model … --config … --manifest created_resources.json --raw-out perf_raw.jsonl --summary-out perf_summary.json
    。脚本会自动处理冷启动纪律(见下文)。
    执行纪律——以前台方式运行并等待。
    representative
    运行会驱动数分钟流量(通常为 settle + 每模式 warmup + 每模式 duration;对于多模式设计往往超过 15 分钟)。必须把基准测试作为单次阻塞调用运行并等待其完成——不要放到后台,也不要把控制权交还用户。后台运行可能在周围轮次或会话结束时被杀,从而留下上一次的
    perf_summary.json
    ——这恰好会导致陈旧摘要被当作新鲜摘要读取。如果运行可能超过工具墙钟限制,应提高工具超时时间——不要缩短窗口,也不要将作业放到后台。你不需要手工控制
    duration_seconds
    以适配预算:重负载运行发生在 Lambda 内部(服务端不超过 900 秒),且
    benchmark_model.py
    已将负载拆分到顺序调用中(
    _compute_split
    )。完成后,记录启动时的 UTC 时间;步骤 5 会将
    perf_summary.json
    的
    benchmark_completed_at
    与其比较。
  5. 报告:
    python3 "$DDB_SKILL_DIR/scripts/generate_perf_report.py" --model … --summary perf_summary.json --output performance_report.md
    。
    在解释报告前,验证摘要既新鲜又与所述实验一致(应用操作纪律)。
    perf_summary.json
    记录
    benchmark_completed_at
    (UTC)以及本次运行实际使用的
    config
    。生成或信任报告前,确认 (a)
    benchmark_completed_at
    晚于步骤 4 中记录的启动时间,且 (b) 摘要中的
    config
    (
    mode
    、
    provisioned_capacity
    、
    duration_seconds
    )与本次设置的参数一致。如果任一条件不满足,或字段缺失,说明基准测试未完成,看到的是陈旧文件:**不要解释,重新运行。**报告的 Deployment 区块会显示
    benchmark_completed_at
    和运行窗口,让用户也能看到这些内容。读取陈旧数据的信号是:基准测试数字与预期不同的先前运行逐字节完全相同。
    **运行你所说的实验——验证配置一致,而不只是新鲜度。**如果你告诉用户本次运行会改变某项内容,例如提高预置容量上限以隔离偏斜、限定模式子集、驱动更高 RPS 或切换模式,则应读取新鲜
    perf_summary.json
    中的实际
    config
    区块,并在报告前确认它确实反映了该变化。如果描述的实验与实际配置不同,应直白说明,并选择重新使用正确配置运行,或说明该实验在物理上无法产生目标信号——例如单位成本或下限运行在约 1 rps 下,无论如何设置容量都不可能驱动任何分区 key 变热,因此永远无法隔离偏斜;此时应使用 representative 模式或按声明峰值/接近峰值驱动;在下限运行中提高预置容量上限不会改变任何结果,这也符合机制 #3 以及下文单位成本/下限运行的说明。绝不能把重新运行描述为测试了一个实际未改变的变量。常见陷阱是:用户发现两次“不同”运行产生几乎相同数字;应主动通过检查配置来避免这一点。
    ⚠ 当用户要求“清理”“拆除”“删除”或“摆脱”基准测试资源时,不要运行
    rm
    、
    rm -f
    、
    ls
    、
    find
    、
    aws dynamodb delete-table
    或任何其他临时删除命令。本 skill 中的“teardown”表示调用
    generate_teardown.py
    生成
    teardown.sh
    ,绝不是自行运行 shell 命令删除本地文件或 AWS 资源。teardown 协议见下文步骤 6。本地交付物文件(
    performance_report.md
    、
    cost_report.md
    、
    dynamodb_data_model.json
    等)由用户自行保留或删除——skill 不负责清理。
  6. **Teardown——先生成,只有在明确审查并表达意图后才执行。**当用户要求拆除、清理或删除基准测试资源时:
    a. 生成脚本:
    python3 ${SKILL_DIR}/scripts/generate_teardown.py --manifest created_resources.json --out teardown.sh
    。这会写入
    teardown.sh
    ,但不会删除任何内容。
    b. **在回复中展示脚本路径、审查命令和执行命令。**使用以下准确形式:
    • 审查:
      cat teardown.sh
      (或在编辑器中打开)。该脚本会删除
      ddb-skill-bench-
      前缀下创建的 DynamoDB 表、Lambda function 和 IAM role。
    • 执行:
      bash teardown.sh --confirm
      ——manifest 中的账户、region 和前缀在生成时已写入脚本,脚本会重新检查调用方身份和前缀后再执行删除。(传入
      --dry-run
      代替
      --confirm
      可预览删除;添加
      --delete-logs
      还会删除 Lambda 的 CloudWatch log group——该日志删除不可逆。)
    c. **不要将“teardown”与“本地清理”混为一谈。**绝不能运行
    rm
    、
    rm -f performance_report.md
    、
    ls
    或
    find
    ,并误以为任务是在删除本地文件。只有
    teardown.sh
    会删除 AWS 资源;本地文件不属于本 skill 的清理范围。
    d. skill 可以代用户执行
    teardown.sh
    ,但必须严格满足以下两部分条件:
    • 用户消息必须说明其已经审查脚本,例如“我读过了”“我审查了 teardown.sh”“我检查了内容”“已经看过”。不能根据上下文推断用户已审查;必须由用户明确陈述。
    • 用户消息必须说明其有意执行,例如“运行它”“执行”“可以,开始拆除”“继续执行 teardown”。
    e. **当用户要求 skill 运行 teardown,但未说明已经审查时,skill 不得执行。**应展示准确的审查命令(
    cat teardown.sh
    ),并请用户确认已审查脚本。所需回复形态为:
    在我运行
    teardown.sh
    前,请确认你已审查该脚本。运行
    cat <PATH>/teardown.sh
    ——它会删除
    ddb-skill-bench-
    前缀下创建的 DynamoDB 表、Lambda function 和 IAM role。阅读后,如需我执行,请回复类似“我已审查,可以执行”,我会运行
    bash teardown.sh --confirm
    。
    f. 当用户要求 skill 跳过审查(“别担心了”“直接运行,不用给我看”“我信任你”)时,拒绝执行。破坏性操作要求用户明确确认已经审查。应说明拒绝原因,并再次提供审查命令。
    g. 满足 (d) 中的条件后,skill 可以运行
    bash teardown.sh --confirm
    ,展示完整的 stdout/stderr,并报告删除了哪些资源。
**操作按声明执行,绝不替换。**设计声明使用
BatchWriteItem
时,基准测试会以每批 25 个项目执行
BatchWriteItem
。设计声明使用
TransactWriteItems
时,基准测试会使用声明的项目集执行
TransactWriteItems
。不会用单项目替代方案,例如用循环
PutItem
代替
BatchWriteItem
,或用单个
PutItem
代替
TransactWriteItems
;这些替代方案测量的不是同一设计。事务 2 倍写入成本(机制 #18)、批处理部分失败语义和事务限制(机制 #14)正是实时验证要观察的内容;替换操作会破坏验证目的。如果用户询问基准测试如何演练批处理或事务模式,应直接说明声明的操作会按原样运行,并指出基准测试将观察到的具体限制。
**冷启动纪律。**新建表的 DynamoDB 延迟和吞吐量在 t=0 并不稳定;如果测量时不遵守纪律,数字会误导性地显得比稳定状态更差。必须消除三类偏差,测量结果才可信:
  • **新表容量。**按需表从基线容量开始(约 2,000 WCU、4,000 RCU);最初几分钟内超过基线的流量可能在自适应容量扩容前被限流。
    table_exists
    返回后,至少等待
    table_settle_seconds
    (默认 30 秒),然后才能发起测量调用。
  • **分区预热和客户端预热。**Seed 写入会预热基准测试随后访问的分区;全新的 boto3 客户端首次调用会经历 TLS 握手和凭据刷新,耗时几十到几百毫秒。基准测试会在 seed 和测量之间短暂暂停,并让每个工作线程执行一次
    DescribeTable
    ,使这些一次性成本不会出现在百分位中。
  • **逐模式预热窗口。**每种模式都会先运行
    warmup_seconds
    窗口(默认 10 秒),然后才进入测量窗口。预热调用会记录为
    phase: "warmup"
    ,并从延迟百分位和成本推算中排除。性能报告会单独展示预热 p99——如果它明显高于稳定状态 p99,这本身就是冷部署行为的特征,值得指出。
测量了什么以及未测量什么:
  • 已测量:通过
    ReturnConsumedCapacity
    获得的逐操作 RCU/WCU 消耗;往返延迟 p50/p95/p99(预热和稳定状态分别报告);限流次数;GSI 写放大;每种模式的均匀分布信号;基于观测值的月度成本推算。
  • 未测量:TTL 清理频率、Streams 消费者延迟和成本、持续突发下的自动扩缩容、跨区域复制延迟、测量窗口以外的长尾流量形态,以及设计中任何非 DynamoDB 服务(Lambda、API Gateway、SQS、EventBridge Pipes、OpenSearch 等)。性能报告会将其列在“Designed, not benchmarked”下,让用户清楚了解数字覆盖了哪些架构部分。
性能报告格式记录在
${SKILL_DIR}/references/performance-report-format.md
。它采用与
cost_report.md
相同的“免责声明优先、表格驱动”风格,并同时打印每种模式的观测数字和计算器预期数字,便于发现 RPS、项目大小、一致性等输入与现实之间的差异。
**反思发现并主动提供迭代循环。**报告写入后,应把它视为诊断工具,而不是最终评分,并询问:是否有测量结果所支持的替代设计?应将每项发现归入以下类别:
  • 输入准确性发现——观测值与计算器结果不同,是因为声明输入(RPS、项目大小、一致性、
    conditional_fail_rate
    )存在偏差。设计本身没有问题,只需更新估算输入。建议更新 JSON 并仅重新运行计算器(成本低,无需重新部署)。
  • 设计发现——无论输入准确性如何,观测结果都揭示了持续存在的结构性问题。示例及相应公理如下:
    • 随分区 key 偏斜而持续扩大的限流 → 机制 #3(逐分区上限,对热点 key 进行写分片)。在把限流归因于热点分区之前,必须通过“偏斜与容量不足判定门”——两个条件都要满足:(1) 差异测试——热点分区 p99 必须明显高于冷分区 p99(报告约 1.8 倍规则)。如果热点 p99 ≈ 冷分区 p99,说明所有分区都均匀限流:这是容量不足,而不是 key 偏斜——应如实说明,也不要归因于分区 key。(2) 统一基线测试——使用报告已经打印的逐操作观测 CU(不要手工推导项目数量),检查在所驱动 RPS 下,统一 key 是否已经超过预置上限;如果会,则本次运行无法隔离偏斜——建议将容量设为高于统一需求后重新运行,使真正的热点分区成为唯一限流来源。只有两个条件都通过,才能得出“热点分区达到其上限”的受支持结论。另需注意,对可变分区 key(例如每次状态转换都会变化的状态)进行写分片会分散机制 #3 的负载,但 key 仍会在每次写入时变化,因此机制 #8(可变 GSI key)偏差依然存在并必须记录在交付物 #5 中;要消除写放大,需要单独的专用表,而不是 hash suffix。
    • 某种模式主导账单 → 机制 #1(聚合绑定紧密程度)、机制 #7(projection)、集成 #8(当工作负载属于分析型、高频遥测或全文检索时,将其移出 DynamoDB,转向 Timestream/S3/OpenSearch/Redshift/SageMaker Lakehouse)。
    • GSI 写放大高于 projection 所暗示的程度 → 机制 #6(可变 key 并不总存在时使用稀疏 GSI)、机制 #7(从 ALL 降级为 INCLUDE/KEYS_ONLY)、机制 #8(可变属性作为 GSI key 会使写入翻倍)。
    • 在并不需要 RYW 语义的地方观察到高读取成本 → 机制 #15(默认最终一致),以及围绕
      ReturnValues=ALL_NEW
      的模式,避免写入后再次读取。
    • 无界查询反复触及 900 KB 分页上限 → 机制 #17(约束每个查询;要求时间窗口或最大页数)。
    • 按需基线下冷启动 p99 明显升高 → 属于容量模式或预热决策(机制 #19),通常不是 schema 变更。
当至少存在一项有意义的设计发现——即建议变更合理地可能降低超过 10% 的成本、消除一个限流来源,或关闭一项公理偏差——应告知用户:
  1. 哪些发现指向设计变更,哪些指向输入准确性。
  2. 对每项设计发现,说明其对应的公理,并提供针对
    dynamodb_data_model.json
    的准确 JSON diff——不能只做文字描述。以带围栏的代码块输出 diff,明确展示要变化的字段,例如新 GSI 对象、删除的 GSI、变更的
    projection.type
    、带 hash suffix 的分片 PK,以及从
    access_patterns
    数组中彻底移出的模式。只给出“考虑降级 projection”之类的口头建议不满足要求——用户必须看到准确变更,才能接受或拒绝。当 diff 变更某个 GSI 的 key(常见情况是分片分区 key)时,依据事实 #9,将其标记为增量 GSI 迁移,而不是只改代码:并行创建新索引、等待填充、切换读取流量,然后删除旧索引。示例形态:
    diff
    - { "name": "OrdersByCustomer", "projection": { "type": "ALL" } }
    + { "name": "OrdersByCustomer", "projection": { "type": "INCLUDE", "non_key_attributes": ["status", "total"] } }
  3. 预期对主要成本驱动因素和限流来源产生的影响。
**口头结论必须继承报告,绝不能超出报告结论。**给用户的聊天总结不得声称报告未分类的结果。具体而言:
  • 如果报告没有输出
    key_skew_patterns
    发现
    ,就不要说设计会“限流”或存在“热点分区”——受支持的表述是报告自身的说法(“在偏斜条件下未观察到热点分区困境”或“本次运行未验证”)。
  • 重述报告的限定语,不要升级。例如,“在刻意设置的较低上限下出现限流”不等于“GSI 分区很热”(参见偏斜与容量不足判定门)。
  • 每个数字都只能用于支撑其实际证明的结论。较低的预置上限导致限流,只能证明表针对所驱动负载配置不足,不能证明任何单个分区 key 很热。
  • 对比示例——相同数据、两种结论:❌ “已确认:OrdersByStatus GSI 在负载下严重限流。”✅ “在 50 WCU 上限下,对 Orders 表的写入发生限流——但热点分区 p99 ≈ 冷分区 p99,因此这是均匀的容量不足,不是 key 偏斜。本次运行没有隔离出热点分区效应;如需测试,应将容量提高到统一需求之上后重新运行。”
  • **单位成本/下限运行——不要将其叙述为负载测试。**当运行属于单位成本样本——
    quick
    /
    standard
    模式,或每种模式的
    bench_rps
    都等于
    min_rps_per_pattern
    下限的运行(
    perf_summary.json
    中会显示统一
    bench_rps
    ,通常约为 1,且报告免责声明写着“UNIT-COST sample, not a load test”)——绝不能将“零限流”或延迟百分位作为设计能够承受负载的证据。在每秒只有少量请求时,零限流在数学上必然成立(按需基线约为 2,000 WCU/约 4,000 RCU),延迟也是无负载情况下的最佳值。必须准确说明本次运行验证了什么——逐操作容量(成本)和无负载延迟——并指出限流/上限“未被测试——请参阅报告的未验证列表”。如果需要让用户放心认为限流不是风险,应给出结构性理由,而不是零限流计数。例如:“高基数分区 key(跨多个用户的
    user_id
    )会将负载分散到不同用户,不会让单个分区变热(机制 #3)。要真正验证峰值行为,应按声明峰值 RPS 驱动,或使用 representative 模式。”
    • 对比示例——约 1 rps 的习惯追踪器运行:❌ “已针对实时 DynamoDB 验证——0 次限流、读取低于 8 ms、写入低于 50 ms,可以进入评审。”✅ “本次运行验证了逐操作成本(与模型相差不超过 2%)和无负载延迟(读取低于 8 ms)。它以约 1 rps 运行,因此没有测试限流——但该设计不存在明显的限流隐患,因为分区 key 是
      user_id
      ,负载会分散到不同用户,不会让单个分区变热。若要证明峰值行为,应按声明峰值 RPS 重新驱动。”
  • 当用户引用任何数字时,将其与自己的最新交付物核对——不要仅仅附和。当用户重述某个数字,例如“每月 189 美元没问题”“所以该分区大约是 600 WCU”“1,884 次限流”,应先对照当前
    cost_report.md
    /
    perf_summary.json
    /
    performance_report.md
    进行核对——在此之前还要确认交付物本身依据步骤 5 保持新鲜。如果不匹配——即使用户语气自信,尤其是希望得到赞同时——应明确纠正并引用交付物:“更正一下:报告写的是
    每月 835 美元
    ,不是 189 美元——189 美元是之前的数字。仍然要继续吗?”不能静默接受错误数字,否则用户会把它带入预算演示或设计评审。成本、容量计算、限流/延迟计数和项目大小假设都适用此规则。用户提供的数字是需要验证的输入,不是应直接认可的结论。
  • **不要为运行未建立的结论背书。**聊天中的结论只能断言当前交付物所展示的内容。如果某个数字来自头脑推算,而不是报告——例如手工推算的预置/预留成本,或从“2k rps 测试线性扩展,所以 25k rps 没问题”得出的结论——应将其标记为推理而非测量,并给出背后的结构性论证——绝不能把估算包装成经过验证的结果。诚实的表达是:“计算器只对按需模式建模;预留数字是我根据公开价格手工计算的 PROVISIONED 替代方案——提交演示材料前,请使用 AWS 计算器确认。”这才是诚实。呈现手算数字时若不注明它是“已验证”,就会误导。
然后询问用户是否希望迭代:应用一项或多项变更并重新评估。提供三个重新评估层级,让用户选择。**必须以三个字面项目符号条目输出——不能合并为一段话,不能遗漏其中任何一项,也不能调整顺序。**用户按名称选择:
  • 仅运行计算器——更新 JSON,重新运行
    calculate_costs.py
    ,展示新的
    cost_report.md
    和并排差异。成本低,不调用 AWS,不做 teardown。这是默认选项,也应最先提供。
  • 计算器 + 实时验证——先执行上述操作,然后针对修改后的设计提供第二次实时运行选项。需要用户重新授权 AWS 部署,并接受第二个 teardown script;除非变更完全是增量式的,否则应先运行之前的
    teardown.sh
    。
  • 不做变更——用户可以合理地认为发现可以接受、变更成本高于收益,或建议的替代方案不符合该 skill 尚不知道的约束。应依据交付物 #5 将该决定记录为一项“偏差”,并写明用户给出的理由。
不要静默迭代,也不要无限迭代。每次迭代都必须明确且由用户选择加入;如果用户拒绝,或报告未发现有意义的问题,就停止并交回最终交付物。

Iterative design loop

迭代设计循环

The three re-eval levels above are run as rounds of a human-driven loop, orchestrated by
${SKILL_DIR}/scripts/iterate_design.py
. This is the seamless path for "deploy → test at representative scale → get feedback → change the design → test again."
What it is — and is NOT. One invocation = one round: optionally apply a user-agreed change → decide reuse-vs-redeploy → (gated) deploy or reuse → benchmark at representative scale → report → surface findings + suggestions → STOP. The loop is human-driven: you run one round, present the findings and concrete suggestions, and hand back. The user decides what to change and prompts the next round. Never self-iterate to convergence; never start a second round in one invocation; the script enforces this (it has no logic to propose or select a change — it only applies the change it is handed).
Representative mode (honest scope). The loop runs
mode: "representative"
(see
${SKILL_DIR}/references/performance-model-schema.md
): ~0.10–0.25× declared peak with zipf hot-key sampling and realistic item-collection cardinality. This surfaces what the 1%-scale unit-cost run cannot — hot-partition throttling (Mechanics #3), throttle-under-load, GSI amplification at volume, and Query-at-realistic-cardinality. State the scope plainly to the user: one in-region Lambda drives ~1,500–2,000 RPS/pattern max (32 driver threads), so representative scale is bounded — it surfaces hot-partition/throttle risk at bounded cost; it does not prove the design sustains declared peak. Per-op cost extrapolation stays linear and valid (per-op CU is scale-invariant, extrapolated against declared peak); the throttle/latency numbers are load-risk signals in the report's "Load-risk signals" section and must never be extrapolated linearly.
Exceeding one Lambda's ceiling (not yet built). If a design needs more than one Lambda's ~1,500–2,000 rps/pattern, the path is to invoke the same deployed benchmark function concurrently (Lambda scales horizontally for free — no need to deploy multiple functions), each invocation driving
target_rps / N
of the load, with seeding done once and the results merged (calls/throttles/errors add; per-op CU is a call-weighted average; latency percentiles are recomputed from the pooled per-call latencies the raw rows already carry). This load-sharding is not implemented yet — today a single Lambda is the ceiling. Don't promise rates above it; offer representative mode or note the ceiling instead.
Gated consent every round. A real deploy and a real teardown each require explicit user consent, every round. The four live-validation facts above (real resources, real charge, sandbox-only, no auto-teardown) apply per round.
iterate_design.py
refuses to deploy without
--yes-deploy
; the benchmark refuses to spend above
cost_guardrail_usd
without
--allow-spend
. The two-phase attested teardown (Live validation step 6) is unchanged and remains the only teardown path — a schema-changing round tells the user to run the prior
teardown.sh
first so no resources are orphaned.
On-demand vs. provisioned, and why throttles may not appear. Representative runs deploy on-demand (PAY_PER_REQUEST) by default, matching the skill's Mechanics #19 default. On on-demand, adaptive capacity isolates and absorbs a single hot key — a hot partition usually shows up as elevated tail latency on that partition, not throttles, because the table auto-scales to serve it. The
key_skew_patterns
signal therefore fires on either throttles or a hot-partition p99 materially above the cold-partition p99 (a baseline-free, within-pattern comparison), so on-demand designs still get the warning. To observe hard throttling, set
provisioned_capacity: {"read": N, "write": M}
in
benchmark_config.json
(or per-table) — a low provisioned ceiling has no adaptive absorption, so a hot partition throttles deterministically; this is also how you validate a planned provisioned capacity. The benchmark Lambda runs with client-side retries disabled so throttles are observed and counted, not silently retried away. Be aware a freshly-created provisioned table carries ~5 min of burst capacity that masks throttling at the very start of a short window — use a longer
duration_seconds
or a lower capacity to drain it.
The round protocol you follow:
  1. Invoke
    python3 "$DDB_SKILL_DIR/scripts/iterate_design.py" --model dynamodb_data_model.json --config benchmark_config.json --loop-state loop_state.json --manifest created_resources.json [--apply-change change.json] --mode representative [--timestamp <iso>] [--yes-deploy] [--allow-spend]
    .
    --timestamp
    is optional — omit it and the round stamps itself with the current UTC time; pass an ISO string only if you need the loop-state entries to carry a specific clock (e.g. reproducible tests). A round runs a representative benchmark, so the same execution discipline as Live validation step 4 applies: invoke it as a single blocking foreground call and wait — never background it (a killed round leaves stale artifacts) — and raise the tool timeout rather than shrinking windows if needed. Before relaying any result, verify freshness as in step 5 (the round's
    loop_state.json
    headline and
    design_findings.json
    derive from the same
    perf_summary.json
    ; if it didn't refresh, the round didn't complete).
  2. Read only the compact artifacts:
    design_findings.json
    ,
    cost_report.md
    , and
    loop_state.json
    . Never read
    perf_raw.jsonl
    (large) or
    performance_report.md
    (human-facing).
  3. Relay the round's compact summary and the delta-vs-prior-round from
    loop_state.json
    (e.g. "sharding the PK cut W1 throttles 1450 → 0, p99 −70 ms"). Present each suggested change as a literal JSON diff on
    dynamodb_data_model.json
    (the same diff requirement as the iteration offer above). Then STOP and ask the user what to change, or whether to accept.
  4. When the user picks a change, pass it as
    --apply-change
    next round (a
    merge
    object or an
    ops
    list); record their decision for the prior round in
    loop_state.json
    's
    user_decision
    .
How the three re-eval tiers map onto the loop: "Calculator only" →
iterate_design.py --calculator-only
(applies the change, re-costs, no AWS); "Calculator + live validation" → a full representative round (gated deploy or reuse); "No changes" → record
user_decision
as a deviation per Artifact #5 and stop. The loop extends the offer, it does not replace it.
quick
/
standard
modes remain for one-shot unit-cost validation;
representative
is the loop's mode. Loop state is documented in
${SKILL_DIR}/references/loop-state-schema.md
.
上述三个重新评估层级通过
${SKILL_DIR}/scripts/iterate_design.py
作为由人驱动的循环执行。这是“部署 → 以代表性规模测试 → 获得反馈 → 修改设计 → 再次测试”的无缝路径。
它是什么,以及不是什么。一次调用 = 一轮:可选地应用用户同意的变更 → 决定复用还是重新部署 →(需授权)部署或复用 → 以代表性规模执行基准测试 → 生成报告 → 呈现发现和建议 → 停止。循环是由人驱动的:运行一轮,呈现发现和具体建议,然后交回控制权。由用户决定修改什么,并提示下一轮。绝不能自行迭代直至收敛;绝不能一次调用中启动两轮;脚本会强制执行这一点(它没有提出或选择变更的逻辑,只会应用传入的变更)。
Representative 模式(真实范围)。循环运行
mode: "representative"
(参见
${SKILL_DIR}/references/performance-model-schema.md
):约为声明峰值的 0.10–0.25 倍,采用 zipf 热点 key 采样和
真实项目集合基数
。它能够暴露 1% 规模单位成本运行无法发现的问题——热点分区限流(机制 #3)、负载下限流、规模化 GSI 放大,以及真实基数下的 Query。必须如实向用户说明范围:单个区域内 Lambda 每个模式最多驱动约 1,500–2,000 RPS(32 个驱动线程),因此代表性规模是有上限的——它能以有限成本暴露热点分区/限流风险,但不能证明设计能够承受声明的峰值。逐操作成本推算保持线性且有效(逐操作 CU 与规模无关,并基于声明的峰值推算);限流/延迟数字属于报告“Load-risk signals”部分中的负载风险信号,绝不能进行线性外推。
超过单个 Lambda 上限(目前尚未实现)。如果设计需要每个模式超过一个 Lambda 约 1,500–2,000 rps 的上限,路径是并发调用同一个已部署的基准测试 function(Lambda 会水平扩缩,且无需部署多个 function),每个调用驱动
target_rps / N
的负载,seed 只执行一次,然后合并结果(调用、限流、错误相加;逐操作 CU 按调用加权平均;延迟百分位根据原始行已经携带的逐调用延迟重新计算)。这种负载分片尚未实现——目前单个 Lambda 就是上限。不要承诺超过该上限的速率;应提供 representative 模式,或说明该上限。
**每轮都需授权。**真实部署和真实 teardown 每轮都需要用户明确同意。上述四项实时验证事实(真实资源、真实费用、仅限 sandbox、不自动 teardown)每轮都适用。
iterate_design.py
没有
--yes-deploy
就会拒绝部署;基准测试没有
--allow-spend
就会拒绝超过
cost_guardrail_usd
的支出。双重 teardown(实时验证步骤 6)保持不变,并且仍是唯一的 teardown 路径——schema 发生变化的轮次应告知用户先运行之前的
teardown.sh
,避免遗留孤立资源。
按需模式与预置模式,以及为何可能看不到限流。Representative 运行默认部署为按需模式(
PAY_PER_REQUEST
),与机制 #19 的默认选择一致。在按需模式下,自适应容量会隔离并吸收单个热点 key——热点分区通常表现为该分区尾延迟升高,而不是限流,因为表会自动扩容以提供服务。因此,
key_skew_patterns
信号会在
出现限流
或热点分区 p99 明显高于冷分区 p99时触发;这是不依赖绝对基线、只比较同一模式内部的信号,因此按需设计仍会收到警告。如需观察硬限流,请在
benchmark_config.json
中设置
provisioned_capacity: {"read": N, "write": M}
(也可按表设置)——较低的预置上限没有自适应吸收能力,因此热点分区会确定性限流;这也是验证计划中的预置容量方式。基准测试 Lambda 禁用客户端重试,以便观察并统计限流,而不是将它们静默重试掉。请注意,新建预置表最初约 5 分钟拥有 burst capacity,会掩盖短窗口开始时的限流——应使用更长的
duration_seconds
或更低的容量来消耗该容量。
你应遵循的轮次协议:
  1. 调用
    python3 "$DDB_SKILL_DIR/scripts/iterate_design.py" --model dynamodb_data_model.json --config benchmark_config.json --loop-state loop_state.json --manifest created_resources.json [--apply-change change.json] --mode representative [--timestamp <iso>] [--yes-deploy] [--allow-spend]
    。
    --timestamp
    是可选的——省略时由本轮写入当前 UTC 时间;仅当需要 loop-state 条目携带特定时钟时(例如可复现测试)才传入 ISO 字符串。一轮会执行 representative 基准测试,因此步骤 4 的相同执行纪律适用:将基准测试作为单次阻塞前台调用并等待完成——绝不能放到后台(被杀死的轮次会留下陈旧交付物)——必要时提高工具超时时间,而不是缩短窗口。在转述任何结果前,按照步骤 5 验证新鲜度(本轮
    loop_state.json
    的标题和
    design_findings.json
    均派生自同一份
    perf_summary.json
    ;如果未刷新,说明本轮未完成)。
  2. 只读取精简交付物:
    design_findings.json
    、
    cost_report.md
    和
    loop_state.json
    。绝不读取
    perf_raw.jsonl
    (文件很大)或
    performance_report.md
    (面向人阅读)。
  3. 转述本轮的精简摘要和
    loop_state.json
    中相对上一轮的变化,例如“分片 PK 将 W1 限流从 1450 降至 0,p99 降低 70 ms”。每项建议变更都应展示为针对
    dynamodb_data_model.json
    的准确 JSON diff(与上述迭代选项的 diff 要求相同)。然后停止并询问用户要修改什么,或是否接受。
  4. 用户选择变更后,在下一轮通过
    --apply-change
    传入,形式为
    merge
    对象或
    ops
    列表;将用户对上一轮的决定记录到
    loop_state.json
    的
    user_decision
    中。
三个重新评估层级如何映射到循环:“仅运行计算器” →
iterate_design.py --calculator-only
(应用变更、重新计算成本、不调用 AWS);“计算器 + 实时验证” → 完整的 representative 轮次(需授权部署或复用);“不做变更” → 依据交付物 #5 将
user_decision
记录为一项偏差并停止。循环扩展了迭代选项,而不是取代它。
quick
/
standard
模式仍用于一次性单位成本验证;
representative
是循环模式。循环状态记录在
${SKILL_DIR}/references/loop-state-schema.md
。

Security considerations

安全注意事项

Security is woven through the axioms above (authorization-aligned keys in Data modeling #14, conditional-write guards in Patterns #2, transactional uniqueness in Mechanics #13). This section consolidates the risks an agent following this skill MUST surface to the user regardless of which axiom is in play, plus the controls specific to the skill's executable scripts. State the relevant ones in any design review; do not assume the user already knows them.
  1. Authorization-boundary alignment (IDOR) is the first security check. The most common DynamoDB data-exposure bug is a partition key set to a client-supplied identifier (a
    review_id
    /
    order_id
    from a URL) instead of the server-derived authorization identifier (
    user_id
    ,
    tenant_id
    ,
    account_id
    ). When they diverge, a caller can pass a foreign key and reach data across the tenancy boundary. Align the partition key with the authorization/tenancy boundary (Data modeling #14) so a foreign key is simply unaddressable; a per-call
    ConditionExpression
    is discipline, not a structural fix, and does not cover reads (Fact #4). On any design review, evaluate and state this before hot-partition, projection, or cost findings.
  2. Encryption at rest — always on; choose the key tier deliberately. DynamoDB encrypts every table at rest with no way to turn it off. The default is an AWS-owned key — no cost, no key management, and the implicit state when no
    SSESpecification
    is set (which is what the live-validation deploy does; the bench manifest records the at-rest state read back from
    DescribeTable
    ). It is right for ephemeral/bench tables and most workloads. For data under a compliance regime (audit of key usage, independent key rotation/revocation, cross-account key policy, or a regulatory requirement to hold the key), recommend a customer-managed KMS key (CMK) — set
    SSESpecification
    with
    SSEType: KMS
    and a
    KMSMasterKeyId
    (note: DynamoDB's only non-default
    SSEType
    is
    KMS
    ; there is no
    AES256
    type on DynamoDB), and note the per-request KMS cost and the need for a key policy. Surface the encryption choice in the design (it is one of the per-entity operational-config inputs) rather than leaving it implicit. Encryption in transit is always on for every AWS API call these scripts make: boto3 talks to DynamoDB/Lambda/IAM over TLS (HTTPS) by default and there is no plaintext path — agents replicating this pattern should treat all data-in-transit as protected and never disable TLS verification.
  3. Generated artifacts are sensitive — treat them accordingly. The deploy writes
    created_resources.json
    , a manifest that contains the AWS account ID, resource ARNs, and the profile name used. It is not a secret store (it holds no credentials), but it identifies the account and resources, so do not paste it into a shared channel, commit it to a public repo, or include it verbatim in a report. The skill's scripts never write AWS credentials, tokens, or connection strings to any artifact — keep it that way; if you extend them, do not log credential material.
  4. Benchmark resources go only in a sandbox/test account. The live-validation deploy creates real, billable resources. It must run only against a sandbox/testing account the user names — never production, never an account holding real user data. The deploy refuses on prod-marker account aliases/ARNs (
    prod
    /
    production
    /
    prd
    /
    live
    ), but the agent is responsible for stating the four live-validation facts and confirming the account's purpose first (see Live validation).
  5. Credential hygiene. Never persist AWS profiles, access keys, session tokens, or
    credential_process
    blobs into any file the skill generates, and never echo them to output. The scripts authenticate from the ambient AWS credential chain (a named profile, SSO, or environment credentials); they neither read arbitrary environment variables wholesale nor record what they resolved. When you write
    benchmark_config.json
    , store only the profile name and region — not the resolved credentials behind them.
安全贯穿于上述公理(数据建模 #14 中与授权对齐的 key,模式 #2 中的条件写入保护,机制 #13 中的事务唯一性)。本节汇总 Agent 遵循本 skill 时,无论涉及哪条公理,都必须向用户说明的风险,以及本 skill 可执行脚本特有的控制措施。应在任何设计审查中说明相关项;不要假设用户已经了解。
  1. **授权边界对齐(IDOR)是第一项安全检查。**最常见的 DynamoDB 数据暴露漏洞,是分区 key 使用客户端提供的标识符,例如 URL 中的
    review_id
    /
    order_id
    ,而不是服务端推导出的授权标识符,例如
    user_id
    、
    tenant_id
    、
    account_id
    。二者不同时,调用方可以传入外部 key,访问租户边界之外的数据。让分区 key 与授权或租户边界对齐(数据建模 #14),使外部 key 根本不可寻址;逐调用
    ConditionExpression
    只是纪律,不是结构性修复,而且不覆盖读取(事实 #4)。任何设计审查中,都应在热点分区、projection 或成本发现之前评估并说明这一点。
  2. **静态加密——始终启用;谨慎选择密钥层级。**DynamoDB 会自动对所有表进行静态加密,无法关闭。默认使用 AWS-owned key——无额外费用、无需密钥管理,也是未设置
    SSESpecification
    时的隐式状态(实时验证部署即采用此方式;benchmark manifest 会记录通过
    DescribeTable
    读回的静态加密状态)。对于临时/benchmark 表和大多数工作负载,这是合适选择。对于受合规制度约束的数据(需要审计密钥使用、独立轮换或撤销密钥、跨账户密钥策略,或监管要求必须持有密钥),建议使用 customer-managed KMS key(CMK)——设置
    SSESpecification
    ,使用
    SSEType: KMS
    和
    KMSMasterKeyId
    (注意:DynamoDB 唯一支持的非默认
    SSEType
    是
    KMS
    ;不存在
    AES256
    类型),并说明逐请求 KMS 成本和密钥策略需求。应在设计中明确体现加密选择(它属于按实体运维配置输入之一),而不是保持隐含。传输中加密始终应用于这些脚本发起的所有 AWS API 调用:boto3 默认通过 TLS(HTTPS)与 DynamoDB/Lambda/IAM 通信,不存在明文路径——复现此模式的 Agent 应将所有传输中数据视为已保护,绝不能禁用 TLS 验证。
  3. **生成的交付物具有敏感性——应适当处理。**部署会写入
    created_resources.json
    ,其中包含所用的 AWS 账户 ID、资源 ARN 和 profile 名称。它不是密钥存储(不保存凭据),但会识别账户和资源,因此不要粘贴到共享渠道、提交到公共仓库,或在报告中原样包含。本 skill 的脚本绝不会将 AWS 凭据、token 或连接字符串写入任何交付物——应保持这一性质;如果扩展脚本,不要记录凭据材料。
  4. **Benchmark 资源只能放在 sandbox/test account 中。**实时验证部署会创建真实且计费的资源。它只能针对用户指定的 sandbox/testing account 运行——绝不能是生产账户,也绝不能包含真实用户数据。部署脚本会拒绝带生产环境标记的账户 alias/ARN(
    prod
    /
    production
    /
    prd
    /
    live
    ),但 Agent 有责任先说明四项实时验证事实并确认账户用途(见实时验证)。
  5. **凭据卫生。**绝不能将 AWS profile、access key、session token 或
    credential_process
    blob 持久化到该 skill 生成的任何文件中,也绝不能将其回显到输出。脚本通过环境中的 AWS 凭据链进行认证(命名 profile、SSO 或环境凭据);既不会任意读取全部环境变量,也不会记录解析出的凭据。编写
    benchmark_config.json
    时,只存储 profile 名称和 region,不要存储其背后的已解析凭据。

Best practices

最佳实践

These are recommended defaults, not axioms. They do not shape the design; they are operational safeguards that should be enabled unless a specific reason argues against them. An agent producing a design should turn each of these on unless the justification for disabling is explicit in the artifacts.
  1. Enable deletion protection on every table. DynamoDB's deletion-protection flag blocks
    DeleteTable
    at the service boundary until it is explicitly disabled. It costs nothing, has no performance impact, and prevents an entire class of incidents — an IaC misconfiguration, a copy-paste command, a stack tear-down that should have been scoped — from destroying a production table. The one-step disable is cheap when deliberate deletion is genuinely intended; the protection should be on everywhere else.
    Two important details people miss:
    • Deletion protection defaults to FALSE. New tables are created without it unless you set
      --deletion-protection-enabled
      on
      create-table
      (or the equivalent property in CloudFormation/CDK or any third-party IaC tool). This is why it is so commonly absent on tables that predate the feature or were created without deliberate thought — you have to explicitly turn it on.
    • Service-boundary protection is not redundant with IaC guards. CDK
      RemovalPolicy.RETAIN
      , a third-party IaC tool's prevent-destroy / retain-on-delete guard, and similar mechanisms only protect the IaC path — a direct
      aws dynamodb delete-table
      CLI call, a different IaC state file targeting the same resource, or a rogue script all bypass them. Deletion protection lives on the table itself at the service boundary and catches every destruction path. Use both together; do not treat IaC guards as sufficient on their own.
  2. Enable point-in-time recovery (PITR) on every table. PITR provides continuous backup with 35-day retention and per-second restore granularity. Cost is a fraction of storage cost, and the day it is needed — accidental deletion, a bad migration, a logic bug that wrote corrupted values — it is the only thing that recovers the data. Disable PITR only on ephemeral tables whose contents can be rebuilt from another source (idempotency caches, dedupe stores with TTL). PITR divergence across tables is also a modeling signal per Data modeling #3.
  3. Make the table observable — alarms and an audit trail for production tables. A table with no monitoring fails silently; the first sign of throttling or a runaway cost is often a customer complaint or the monthly bill. For every production table, recommend:
    • CloudWatch alarms on
      ThrottledRequests
      ,
      SystemErrors
      , and
      UserErrors
      (and, on provisioned tables, on consumed-vs-provisioned capacity). At minimum a starter alarm on
      ThrottledRequests > 0
      sustained — that is the earliest signal of a hot partition or under-provisioning, and it ties directly to the Mechanics #3 ceilings.
    • CloudTrail data-event logging for tables holding sensitive or regulated data, so item-level reads and writes are auditable. CloudTrail captures DynamoDB management events by default; item-level (data) events are opt-in per table and are what an audit or incident investigation actually needs. When the audit/operational logs themselves hold sensitive data, encrypt the CloudWatch Logs log group (and the CloudTrail trail's S3 bucket) with an AWS KMS customer-managed key so the logged data is protected at rest, not just the source table.
    These are operational safeguards, not design inputs — but because this skill's guidance is replicated across many customer environments, surfacing them is part of a defensible design rather than an afterthought.
以下是推荐默认值,不是公理。它们不塑造设计,而是应启用的运营保护措施,除非存在明确理由需要关闭。产出设计的 Agent 应默认启用这些措施,并仅在交付物中明确说明关闭理由时才不启用。
  1. **为每张表启用删除保护。**DynamoDB 的删除保护标志会在服务边界阻止
    DeleteTable
    ,直至显式关闭。它不产生费用、没有性能影响,并可防止一整类事故——IaC 配置错误、复制粘贴命令、超出预期范围的栈拆除——摧毁生产表。当确实有意删除时,只需单步关闭保护,成本很低;其他情况下应始终启用。
    两个经常被忽略的重要细节:
    • **删除保护默认是 FALSE。**除非在
      create-table
      时设置
      --deletion-protection-enabled
      (或在 CloudFormation/CDK 或其他第三方 IaC 工具中使用等效属性),否则新表创建后没有该保护。这就是它为何在许多早于该功能发布或创建时未明确决策的表中普遍缺失——必须显式开启。
    • **服务边界保护不能由 IaC 防护替代。**CDK
      RemovalPolicy.RETAIN
      、第三方 IaC 工具的 prevent-destroy/retain-on-delete 防护等机制只保护 IaC 路径;直接执行
      aws dynamodb delete-table
      CLI、使用该表的其他 IaC 状态文件,或恶意脚本,都能绕过它们。删除保护位于表自身的服务边界,可以拦截所有销毁路径。两者应结合使用;不要认为 IaC 防护已经足够。
  2. **为每张表启用 point-in-time recovery(PITR)。**PITR 提供连续备份,保留 35 天,并支持逐秒恢复粒度。成本只占存储成本的一小部分,而真正需要它时——意外删除、错误迁移、写入损坏值的逻辑 Bug——它是唯一能够恢复数据的手段。只有内容可从其他来源重新构建的临时表,才应关闭 PITR,例如幂等缓存和带 TTL 的去重存储。PITR 在表之间的差异也是依据数据建模 #3 识别出的建模信号。
  3. **让表具备可观测性——生产表应配置告警和审计轨迹。**没有监控的表会静默失败;限流或成本失控的第一个信号,往往是客户投诉或月度账单。对于每张生产表,建议:
    • 对
      ThrottledRequests
      、
      SystemErrors
      和
      UserErrors
      配置 CloudWatch alarms;对于预置表,还应监控已消耗容量与预置容量的关系。至少应设置一个初始告警:当
      ThrottledRequests > 0
      持续出现时触发——这是热点分区或容量配置不足的最早信号,也直接对应机制 #3 的上限。
    • 对保存敏感或受监管数据的表启用 CloudTrail data-event logging,以便审计逐项目读写。CloudTrail 默认捕获 DynamoDB 管理事件;逐项目(数据)事件必须按表选择启用,而它们才是审计或事故调查真正需要的内容。当审计或运营日志本身包含敏感数据时,应使用 AWS KMS customer-managed key 加密 CloudWatch Logs log group(以及 CloudTrail trail 的 S3 bucket),确保日志静态数据受到保护,而不只是源表受到保护。
    这些属于运营保护措施,而不是设计输入——但由于本 skill 的指导已复制到许多客户环境中,将它们说明清楚是形成有充分依据设计的一部分,而不是事后补充。

Reference architectures

参考架构

Worked examples that apply these axioms end-to-end to a concrete application. Consult when a design question is easier to answer against a running model than against the axioms alone.
  • ${SKILL_DIR}/references/reference-architecture.md
    — a multi-tenant kanban task-board ("TaskBoard") SaaS on AWS backed by DynamoDB. Covers the access-pattern list, table and GSI layout, Streams fan-out to OpenSearch and EventBridge, authorization-aligned partitioning, real-time delivery, and the trade-offs made at each step. Start here when a user asks about a full-stack DynamoDB design, multi-tenancy, or end-to-end composition of the services around the database.
  • ${SKILL_DIR}/references/cost-model-schema.md
    — the JSON shape consumed by
    ${SKILL_DIR}/scripts/calculate_costs.py
    . Read this the first time you produce a monthly cost estimate (see Cost estimation above). Skip it for non-cost questions.
  • ${SKILL_DIR}/references/performance-model-schema.md
    — the JSON shape of
    benchmark_config.json
    consumed by the live-validation scripts. Read this the first time you run a live validation (see Live validation above). Skip it when live validation is not on the table.
  • ${SKILL_DIR}/references/performance-report-format.md
    — the structure of
    performance_report.md
    generated by
    ${SKILL_DIR}/scripts/generate_perf_report.py
    . Read this before interpreting the report or drafting the design-reflection section.
  • ${SKILL_DIR}/references/loop-state-schema.md
    — the shape of
    loop_state.json
    , the compact per-round genealogy file written by
    ${SKILL_DIR}/scripts/iterate_design.py
    . Read this when running the iterative design loop (see Iterative design loop above).
以下工作示例将这些公理端到端应用于具体应用。当设计问题对照一个正在运行的模型比仅对照公理更容易回答时,应查阅这些示例。
  • ${SKILL_DIR}/references/reference-architecture.md
    ——一个多租户 kanban 任务板(“TaskBoard”)SaaS,运行在 AWS 上并以 DynamoDB 为后端。涵盖访问模式列表、表和 GSI 布局、面向 OpenSearch 和 EventBridge 的 Streams 扇出、与授权对齐的分区、实时投递,以及围绕数据库的各项权衡。当用户询问完整栈 DynamoDB 设计、多租户或数据库周边服务的端到端组合时,应从此处开始。
  • ${SKILL_DIR}/references/cost-model-schema.md
    ——
    ${SKILL_DIR}/scripts/calculate_costs.py
    所消费的 JSON 结构。首次产出月度成本估算时阅读本文件(见上文成本估算)。非成本问题可跳过。
  • ${SKILL_DIR}/references/performance-model-schema.md
    ——实时验证脚本所消费的
    benchmark_config.json
    JSON 结构。首次运行实时验证前阅读本文件(见上文实时验证)。未计划实时验证时可跳过。
  • ${SKILL_DIR}/references/performance-report-format.md
    ——
    ${SKILL_DIR}/scripts/generate_perf_report.py
    生成的
    performance_report.md
    结构。在解释报告或撰写设计反思部分之前阅读本文件。
  • ${SKILL_DIR}/references/loop-state-schema.md
    ——
    ${SKILL_DIR}/scripts/iterate_design.py
    生成的紧凑逐轮谱系文件
    loop_state.json
    的结构。运行迭代设计循环时阅读本文件(见上文迭代设计循环)。