# DynamoDB — Part 1: Data Model and Access

## 1. What DynamoDB Is

**Amazon DynamoDB** is a fully managed, serverless **key-value and document** database with single-digit-millisecond latency at virtually any scale. No servers, no connection limits (HTTP API), automatic replication across 3 AZs, optional global tables across regions. The trade-off: you must design the data model around your **access patterns** up front — there are no joins and no ad-hoc queries over arbitrary attributes.

## 2. Core Concepts

| Concept | Meaning |
|---|---|
| **table** | collection of items (no fixed schema except the key) |
| **item** | a record, up to **400 KB**, with attributes (string, number, binary, bool, list, map, set) |
| **partition key (PK)** | required; hashed to choose the physical partition |
| **sort key (SK)** | optional; items with the same PK are stored sorted by SK — enables range queries |
| **primary key** | PK alone, or PK + SK (must be unique) |
| **GSI** (global secondary index) | alternative PK/SK over the same data, eventually consistent, own capacity |
| **LSI** (local secondary index) | same PK, different SK; must be defined at table creation; 10 GB per PK limit |

## 3. Reading: GetItem, Query, Scan

```ts
import { DynamoDBDocumentClient, GetCommand, QueryCommand } from "@aws-sdk/lib-dynamodb";

// GetItem: exact primary key → 1 item
await ddb.send(new GetCommand({ TableName: "app", Key: { PK: "USER#42", SK: "PROFILE" } }));

// Query: one partition key, optional sort-key condition → items in SK order (paginated, 1 MB per page)
await ddb.send(new QueryCommand({
  TableName: "app",
  KeyConditionExpression: "PK = :pk AND begins_with(SK, :prefix)",
  ExpressionAttributeValues: { ":pk": "USER#42", ":prefix": "ORDER#2025-" },
  ScanIndexForward: false,      // newest first
  Limit: 20,
}));
```

- **Query** is the workhorse: efficient because it reads one partition, ordered by SK.
- **Scan** reads the whole table (every partition) — expensive and slow; fine for small tables, backfills or exports, never for request paths.
- **FilterExpression** is applied **after** reading — you still pay for everything read. Design keys so filters are rarely needed.
- Reads are **eventually consistent** by default (half the cost); `ConsistentRead: true` for strongly consistent reads (not available on GSIs).

## 4. Writing

```ts
await ddb.send(new PutCommand({
  TableName: "app",
  Item: { PK: "USER#42", SK: "ORDER#2025-06-12#o-9", total: 4599, status: "PLACED" },
  ConditionExpression: "attribute_not_exists(PK)",           // create only (idempotent creates, no overwrite)
}));

await ddb.send(new UpdateCommand({
  TableName: "app",
  Key: { PK: "PRODUCT#7", SK: "STOCK" },
  UpdateExpression: "SET qty = qty - :n",
  ConditionExpression: "qty >= :n",                          // atomic, no oversell (advanced-engineering/…/02-race_conditions)
  ExpressionAttributeValues: { ":n": 2 },
}));
```

- **Conditional writes** give optimistic concurrency (version attributes) and invariants without locks.
- **TransactWriteItems**: up to 100 items across tables, all-or-nothing (2× cost).
- **BatchWriteItem / BatchGetItem**: up to 25 / 100 items per call, not atomic, retry `UnprocessedItems`.
- **TTL attribute**: items expire automatically (deletion within ~48 h, free) — sessions, idempotency keys, temporary data.

## 5. Capacity Modes

| Mode | How | Use for |
|---|---|---|
| **on-demand** | pay per request (RRU/WRU); scales automatically | unknown/spiky traffic, new apps |
| **provisioned** | set RCU/WCU (+ auto scaling); cheaper at steady load | predictable traffic |

1 WCU = one write/s of ≤ 1 KB; 1 RCU = one strongly consistent read/s of ≤ 4 KB (or two eventually consistent). Item size drives cost — keep items small, project only needed attributes into GSIs.

## Under the Hood: Partitions and Hot Keys

DynamoDB hashes the partition key to place items on **partitions** — storage nodes each holding up to ~10 GB and serving up to about **3,000 RCU and 1,000 WCU** per second. Each partition is replicated to three nodes in three AZs; writes are acknowledged after a quorum (two of three) persists them, with a leader per partition handling writes and strongly consistent reads. As data or throughput grows, DynamoDB **splits** partitions automatically, and **adaptive capacity** shifts throughput toward busy partitions. What it can't fix is a single **hot key**: all requests for `PK = "GLOBAL_COUNTER"` land on one partition, capping it at ~1,000 writes/s regardless of table capacity → `ProvisionedThroughputExceededException` / throttling. Spread load with high-cardinality partition keys or **write sharding** (`COUNTER#<0..9>` and sum on read) — the same idea as sharded counters in `databases/caching/08-distributed_caching`.

![A request's partition key is hashed to one of many partitions replicated across three availability zones; items sharing a partition key are sorted by sort key for efficient queries, while a single hot key saturates one partition's limits](diagrams/01-partitions.svg)

## Quick Reference (Part 1)

```
PK (+SK) · item ≤ 400 KB · GetItem (exact) · Query (one PK, SK range, sorted) · Scan (avoid in request paths)
filters cost what they read · eventually consistent default · ConditionExpression for invariants · transactions ≤ 100 items
on-demand vs provisioned · TTL for expiry · hot keys cap at ~1k WCU/3k RCU per partition → shard keys
