---
cuppy_hub: aws
cuppy_path: guides/dynamodb/design-and-access.md
cuppy_section: docs
cuppy_doc: SHR9-MGGP
cuppy_doc_uuid: c26f0af4-2d9a-44aa-8343-65fe1a055866
cuppy_permalink: https://cuppy.io/aws/d/SHR9-MGGP
cuppy_version: 4
cuppy_updated: 2026-10-23T16:16:52.575Z
cuppy_canonical: https://cuppy.io/aws/docs/guides/dynamodb/design-and-access
cuppy_mcp: https://cuppy.io/mcp
cuppy_body_sha256: fd8bda44600a086c70be817158252968dc1618f0234827dba1e7a80f583cc759
# cuppy_body_sha256 is over the document body ALONE, excluding both generated blocks.
---


# Design a DynamoDB table and read and write it correctly

**Start from the queries, not the entities.** Every frequent read should be a `GetItem` or a `Query` on a key: the table's own key or a secondary index. Never a `Scan`. Use **on-demand** capacity unless the load is steady and forecastable: AWS recommends on-demand for most workloads.

## 1. Keys from access patterns

| Access pattern | Key design |
|---|---|
| Fetch one thing by ID | Partition key = ID |
| All of X's items, sorted or ranged (a customer's orders by date) | Partition key = X, sort key = sortable string (`o#2026-10-18#001`) |
| The same items by another attribute (orders by status) | Global secondary index (GSI) on that attribute |
| Another sort order within the same partition | Local secondary index (LSI), defined at table creation |

Fixed constraints that shape design:

- **Items** are at most 400 KB, including attribute names. Nesting goes 32 levels deep.
- **Partition keys** are 1–2048 bytes; **sort keys** 1–1024 bytes.
- With an **LSI**, each partition key's item collection is limited to **10 GB** (`ItemCollectionSizeLimitExceededException`). Up to 5 LSIs per table.
- **GSIs:** the number per table is an adjustable quota. Read the live value in Service Quotas rather than assuming one. There are 100 projected attributes in total across indexes.
- **Query and Scan** return at most **1 MB per call**. Paginate with `LastEvaluatedKey`.
- **Batches:** BatchWriteItem takes 25 items / 16 MB, BatchGetItem 100 items / 16 MB. **Transactions** take 100 items / 4 MB.
- **Expressions** are at most 4 KB, with 300 operators, and an `IN` list of up to 100 values.

Spread writes across partition key values. A single "hot" key throttles even when the table has capacity (UNVERIFIED mechanism detail; the error page attributes throttling to exceeding a table or GSI's throughput).

## 2. Capacity mode

- **On-demand** (the default): pay per request, no planning. It can still return `RequestLimitExceeded` at the account's throughput limit.
- **Provisioned:** you set reads and writes per second and pay for the provisioned hours. It suits steady, predictable workloads. Exceeding it gives `ProvisionedThroughputExceededException`.

## 3. Read and write from TypeScript

```ts
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { DynamoDBDocumentClient, PutCommand, QueryCommand, UpdateCommand } from "@aws-sdk/lib-dynamodb";

const ddb = DynamoDBDocumentClient.from(new DynamoDBClient({ region: "<region>" }));
const TableName = "orders"; // PK = customerId, SK = orderId

// Create only if absent: a conditional write instead of read-then-write.
await ddb.send(new PutCommand({
  TableName,
  Item: { customerId: "c#42", orderId: "o#2026-10-18#001", status: "NEW", total: 1999 },
  ConditionExpression: "attribute_not_exists(orderId)",
}));

// All of one customer's October orders, newest first, one page at a time (1 MB per page).
let ExclusiveStartKey: Record<string, unknown> | undefined;
do {
  const page = await ddb.send(new QueryCommand({
    TableName,
    KeyConditionExpression: "customerId = :c AND begins_with(orderId, :p)",
    ExpressionAttributeValues: { ":c": "c#42", ":p": "o#2026-10" },
    ScanIndexForward: false,
    ExclusiveStartKey,
  }));
  for (const item of page.Items ?? []) console.log(item.orderId, item.status);
  ExclusiveStartKey = page.LastEvaluatedKey;
} while (ExclusiveStartKey);

// Optimistic state change: fails with ConditionalCheckFailedException if someone moved it first.
await ddb.send(new UpdateCommand({
  TableName,
  Key: { customerId: "c#42", orderId: "o#2026-10-18#001" },
  UpdateExpression: "SET #s = :next",
  ConditionExpression: "#s = :expected",
  ExpressionAttributeNames: { "#s": "status" }, // "status" is a reserved word
  ExpressionAttributeValues: { ":next": "PAID", ":expected": "NEW" },
}));
```

- The **document client** converts plain JavaScript values to DynamoDB attribute values. The low-level client needs `{ S: "…" }` forms.
- **Conditional writes** (`attribute_not_exists`, `#s = :expected`) replace read-then-write races. A failed condition is `ConditionalCheckFailedException`, a normal outcome to handle, **not to retry**.
- **Reserved words** such as `status` must be aliased through `ExpressionAttributeNames` (the API model's `ExpressionAttributeNames` documentation names reserved-word conflicts as the first use). A reserved word used unaliased is a common `ValidationException` cause.
- **Pagination:** always loop on `LastEvaluatedKey`. `Limit` is "the maximum number of items to evaluate (not necessarily the number of matching items)": with a filter, a page can come back short or empty while `LastEvaluatedKey` says there is more (API model).
- **Batches:** retry `UnprocessedItems` / `UnprocessedKeys` with backoff. They are partial successes, not errors.

## 4. Permissions

The caller needs `dynamodb:PutItem`, `Query`, `UpdateItem` and so on, on `arn:aws:dynamodb:<region>:<account>:table/<name>`, plus `…/index/*` for index queries (UNVERIFIED ARN suffix in the collected sources). Add `kms:Decrypt` and `kms:GenerateDataKey` if the table uses a customer managed KMS key (`reference/kms/key-policies-and-errors.md`).

## Next

- Error by name: `reference/dynamodb/errors.md`.
- Reacting to changes: `patterns/dynamodb-streams-to-lambda.md`.
- Expiring items, backups and restores, multi-Region: `reference/dynamodb/ttl-pitr-and-global-tables.md`.


---
<!-- cuppy -->
You are reading one file. The same content is free over MCP at https://cuppy.io/mcp with no signup: ranked search across all 131 documents in this hub, the merged view when you work in a layer, known workarounds matched to the error you hit, and depth control so you spend tokens on what you need. Start with the `_hello` tool. How to connect: https://cuppy.io/aws/ Something wrong in this document? Report it with the `session feedback` tool on the same endpoint.
<!-- /cuppy -->
