DynamoDB
DynamoDB is a database of items looked up by key. A read or write by key takes a few milliseconds whether the table holds a hundred items or a billion, and you never size a server for it: by default you pay per request.
That speed comes from one rule: you can only look items up by their keys. So designing a table starts from the questions you’ll ask it.
A table is items under keys
Section titled “A table is items under keys”An item is a set of attributes, like a row without a fixed schema. Every item has a partition key, and DynamoDB stores items by it: all items with the same partition key live together, and different partition keys spread across many machines.
A table can also have a sort key. Then the partition key and the sort key together identify an item, and the items in one partition are kept in order by the sort key.
| pk | sk | total |
|---|---|---|
| user#ana | order#0301 | $8 |
| user#ana | profile | · |
| user#sam | order#0104 | $20 |
| user#sam | order#0211 | $35 |
| user#sam | profile | · |
Two kinds of read are fast. Get one item by its full key. Query one partition, optionally narrowing by a range of sort keys: “Sam’s orders in 2026”. Anything else means a scan, which reads the whole table. So list the reads your app needs before you choose the keys, and pick keys that turn each one into a get or a query.
Declare a table
Section titled “Declare a table”Name the key attributes and their types. "S" is a string, "N" a number, "B" binary:
import * as AWS from "@alchemy.run/aws";
export const Orders = AWS.DynamoDB.Table("Orders", { partitionKey: "pk", sortKey: "sk", attributes: { pk: "S", sk: "S" },});attributes lists only the attributes used in keys and indexes. Every other attribute of an item is free-form.
The table is created on-demand: there’s no capacity to set, and you pay per read and write. Alchemy names it from your app, stage and id, so each stage gets its own table.
Write items
Section titled “Write items”Bind DynamoDB.PutItem in a runtime. Its role gets dynamodb:PutItem on this table only:
const putItem = yield* AWS.DynamoDB.PutItem(Orders);An item is written in DynamoDB’s own attribute format, where each value says its type:
yield* putItem({ Item: { pk: { S: "user#sam" }, sk: { S: "order#2026-02-11" }, total: { N: "42" }, },});The bindings pass this format through unchanged, so what you write is exactly what the API receives. If you’d rather work with plain objects, wrap the client in a function that converts them.
A put replaces any item with the same key. To change some attributes and keep the rest, use DynamoDB.UpdateItem. To write only if the item doesn’t exist yet, add a ConditionExpression, and catch ConditionalCheckFailedException when it does.
Read an item
Section titled “Read an item”Bind DynamoDB.GetItem and read by the full key:
const getItem = yield* AWS.DynamoDB.GetItem(Orders);const { Item } = yield* getItem({ Key: { pk: { S: "user#sam" }, sk: { S: "profile" } },});A missing item isn’t an error: Item is just undefined.
Each binding has a client Layer named after it. Provide the ones you bind to the runtime, merged into one:
Effect.provide(Layer.mergeAll(AWS.DynamoDB.GetItemHttp, AWS.DynamoDB.PutItemHttp))Query a partition
Section titled “Query a partition”Bind DynamoDB.Query to read many items from one partition. The key condition names the partition and, optionally, a range of sort keys:
const query = yield* AWS.DynamoDB.Query(Orders);const { Items } = yield* query({ KeyConditionExpression: "pk = :pk AND begins_with(sk, :prefix)", ExpressionAttributeValues: { ":pk": { S: "user#sam" }, ":prefix": { S: "order#2026" }, },});Items come back in sort-key order, a page at a time. This is why the sort key above starts with order# and a date: one query returns a user’s orders for a year, already sorted.
Add an index for another question
Section titled “Add an index for another question”Some reads don’t fit the table’s keys: “every order placed today”, across all users. A global secondary index answers them. It’s a copy of the table, kept in sync for you, with its own partition and sort key:
export const Orders = AWS.DynamoDB.Table("Orders", { partitionKey: "pk", sortKey: "sk", attributes: { pk: "S", sk: "S", day: "S" }, globalSecondaryIndexes: [ { indexName: "byDay", partitionKey: "day", sortKey: "sk", projection: { ProjectionType: "ALL" } }, ],});Query it by passing IndexName: "byDay" to the same query client. An index is updated shortly after each write, so a query on it may briefly miss the newest items.
Each index costs storage and a write for every item it covers. Add one per question that needs it, not one per attribute.
Expire items with TTL
Section titled “Expire items with TTL”Sessions, caches and one-time codes should disappear on their own. Name an attribute that holds an expiry time, in seconds since 1970, and DynamoDB deletes each item some time after it passes:
export const Sessions = AWS.DynamoDB.Table("Sessions", { partitionKey: "id", attributes: { id: "S" }, timeToLiveSpecification: { AttributeName: "expiresAt", Enabled: true },});Deletion can lag the expiry by a while, so check expiresAt when you read too.
React to changes
Section titled “React to changes”A table can publish every change to its items, in order per item, as a stream. A Function consumes it to keep a search index current, send notifications or update totals. See DynamoDB Streams.
Deploy changes to a table
Section titled “Deploy changes to a table”Most settings change in place: indexes, TTL, tags, streams and recovery are updated on the existing table. Changing the partition key, the sort key or a local secondary index replaces the table, because DynamoDB can’t change them, and replacing a table loses its items.
Protect a table that holds data you can’t recreate. Turn on point-in-time recovery to restore it to any second in the last 35 days, and deletion protection to refuse a delete until you turn it off:
export const Orders = AWS.DynamoDB.Table("Orders", { partitionKey: "pk", sortKey: "sk", attributes: { pk: "S", sk: "S" }, pointInTimeRecoverySpecification: { pointInTimeRecoveryEnabled: true }, deletionProtectionEnabled: true,});