15.5
DynamoDB Indexes, DAX, Streams and Global Tables
LSI vs GSI, query vs scan, DAX, Streams, Global Tables, TTL, backups and S3 export/import.
28 min Reading + animations
Tip: tap or hover any dotted-underlined term to see what it means.
By the end of this lesson you will be able to
- Answer a new question about a DynamoDB table without a Scan: choose between a Query on the table, a local secondary index (LSI) and a global secondary index (GSI), and know each one’s price (when it can be created, keys, consistency, capacity, the 10 GB item collection limit, how many per table).
- Explain why an under-provisioned GSI throttles writes on the base table, and pick projections that keep indexes small.
- Put DAX in front of a read-heavy table, predict cache hits, misses and stale results (item cache, query cache, write-through, strongly consistent pass-through, 5-minute default TTL), and tell DAX apart from ElastiCache.
- Use DynamoDB Streams with Lambda for event-driven work (welcome emails, derived tables, audit), handle a record that keeps failing, and know when Kinesis Data Streams for DynamoDB is the better change log (1-year retention, more consumers).
- Design with Global Tables: active-active replication, last writer wins, Region failover, and the 2025 multi-Region strong consistency mode.
- Expire data for free with TTL, undo mistakes with PITR (1 to 35 days) and on-demand backups or AWS Backup, and analyse or load data with export to S3 and import from S3 without spending table capacity.
- Place the remaining integrations: zero-ETL to Redshift and OpenSearch, the VPC gateway endpoint, and fine-grained access with
dynamodb:LeadingKeys.
#Where this lesson starts
Lesson 15.4 built the core: tables, items up to 400 KB, a primary key made of a partition key and an optional sort key, read and write capacity units, eventually and strongly consistent reads, and the difference between a Query (reads one partition key value, cheap) and a Scan (reads the whole table, slow and expensive). Its exam rule was blunt: for a “cost-effective and performant way to search”, never pick Scan.
That rule raises the obvious question this lesson answers first: what if the app needs to search by something that is not the primary key? After indexes come the features that turn a table into a building block: a cache (DAX), a change log (Streams), copies in other Regions (Global Tables), automatic expiry (TTL), backups, and a door to S3.
#Secondary indexes: other ways to read one table
DynamoDB can only Query by key. A secondary index is a second copy of (some of) the table’s data, organised by a different key, which DynamoDB keeps up to date for you. You Query the index exactly as you would Query the table, with --index-name.
Take an e-commerce shop’s Orders table:
- Partition key
user_id, sort keyorder_date. - “User_101’s orders, newest first” is one Query on the table (
ScanIndexForward=falsereads the sort key backwards).
Customer care now asks for two more things: user_101’s most expensive orders first, and an order found by its tracking number when that is all the caller knows. The table is sorted by date, not price, and it is keyed by user, not tracking number. Each question gets its own kind of index.
#Local secondary index (LSI)
An LSI keeps the same partition key as the table and adds a different sort key. For the price question: partition key user_id (the same), sort key price (new). A Query on the LSI with user_id = user_101 and descending order returns that customer’s orders, most expensive first.
Because the partition key is the same, the LSI’s entries live in the same partition as the items they point to, right next to them. That is why it is called local, and it explains every rule that follows:
- It must be defined when the table is created. You cannot add one later, and you cannot delete it without deleting the table.
- It uses the table’s own read and write capacity; it has none of its own.
- It is updated in the same operation as the base item, so it supports strongly consistent reads as well as eventually consistent ones.
- At most 5 LSIs per table.
- When a table has an LSI, all the items that share one partition key value, in the table and its LSIs together, form an item collection, and an item collection may not grow beyond 10 GB. The notes say “User_101’s data cannot exceed 10 GB”: that is exactly it, and it only applies to tables with an LSI.
#Global secondary index (GSI)
A GSI can use any attribute as its partition key, with an optional sort key of its own. For the tracking question: partition key tracking_no. It is a separate, re-keyed copy of the data spread over its own partitions, which is why it is called global.
- It can be added at any time, also to a table that has been running for months (DynamoDB backfills it from the existing items while the table stays online), and deleted at any time.
- It has its own capacity (provisioned RCU and WCU, or on-demand like the table).
- It is updated asynchronously, usually within a fraction of a second. Reads are eventually consistent only; asking for a strongly consistent read on a GSI is an error.
- No size limit per key value.
- Default quota 20 GSIs per table (a soft limit you can raise).
- Unlike a table, a GSI’s key values need not be unique: many orders can share one
status, and a Query on the index returns them all.
Since 2025 a GSI key can also be a multi-attribute key: up to four attributes for the partition key and four for the sort key, instead of building a combined string such as status#date yourself. The classic answer, a composite string attribute, still works and is what older exam questions expect.
#LSI vs GSI at a glance
This is the owner’s exam table, checked against the current DynamoDB documentation:
| LSI (local secondary index) | GSI (global secondary index) | |
|---|---|---|
| Partition key | Same as the table (user_id) | Any attribute (tracking_no, email) |
| Sort key | Different from the table (price) | Optional, any attribute (order_date) |
| When created | Only at table creation | Any time, at creation or later (backfilled) |
| When deleted | Only by deleting the table | Any time |
| Capacity | Shares the table’s RCU / WCU | Its own RCU / WCU |
| Size limit | 10 GB per item collection (one partition key value, table + LSIs) | None |
| Read consistency | Strong and eventual | Eventual only |
| How many | 5 per table | 20 per table (default quota, adjustable) |
#Projections: what the index copies
An index stores the table’s key attributes and the index key, plus whatever you project:
KEYS_ONLY: the smallest index; to show more, fetch the full item from the table by its key.INCLUDE: keys plus a list of named attributes (for examplestatusandpricefor the support screen).ALL: every attribute; no second lookup needed, but the index is as big as the table and every write costs as much on the index as on the table.
A query on an LSI may still ask for an attribute that was not projected: DynamoDB fetches it from the table for you, at extra read cost. A GSI cannot do that: only projected attributes come back. Project what the access pattern needs, nothing more.
#The GSI write trap: back-pressure
The notes’ table says a GSI “does not use the main table’s capacity”. True, and it hides the most-tested GSI fact. Every write to the table that touches an indexed attribute must also be written to the GSI, and that costs the GSI’s write capacity. If the GSI runs out, DynamoDB throttles writes on the base table (ProvisionedThroughputExceededException), even when the table itself has plenty of spare capacity. This is called back-pressure.
So, in provisioned mode, give every GSI at least the same write capacity as the table (or put both under auto scaling), or run the table in on-demand mode, where the index scales with it. Reads are the other way round: a GSI that nobody reads still has to absorb every write.
A second trap is a low-cardinality GSI key. A GSI keyed on status has a handful of values, so on a sale day nearly every write lands in the shipped partition of the index: a hot partition in the index, with the same back-pressure on the table. Spread it with a suffix (shipped#0 … shipped#9) and query the shards in parallel, the same write-sharding idea as the hot partition in lesson 15.4.
LSI and GSI: two other ways to read one table
An e-commerce shop keeps orders in one table keyed by user_id (partition key) and order_date (sort key). Customer care now wants other questions answered: a customer’s most expensive orders, and an order found by its tracking number.
Scenario
Same partition key, different sort key, strongly consistent reads allowed.
Orders table, partition user_101
| user_id | order_date | price |
|---|---|---|
| user_101 | 2026-03-02 | 1,200 |
| user_101 | 2026-05-18 | 45,000 |
| user_101 | 2026-08-09 | 3,500 |
The table keeps each customer’s orders together and sorted by order_date. “User_101’s orders, newest first” is one cheap Query. “User_101’s most expensive orders first” is not: the table is not sorted by price.
Now plan your own index. Pick a question the app must answer and three facts about the table; the planner tells you what serves it, the keys, the rows that come back from a small sample, and the caveats.
Index planner: which index answers this question?
An e-commerce shop’s orders table has user_id as partition key and order_date as sort key. Pick a question the app must answer and what you know about the table, then plan it. The sample table has 8 orders; production has 2 million.
The question
Table already running for months?
Strongly consistent read needed?
Can one customer pass 10 GB of orders?
Press “Plan it” to see the result.
#DAX: microseconds without changing your code
DynamoDB answers in single-digit milliseconds. For most apps that is plenty. But some workloads read the same items over and over: a breaking news article, a product page during an Eid flash sale, a game’s leaderboard settings. Every one of those reads costs read capacity, and a single very hot key can even throttle its partition.
Amazon DynamoDB Accelerator (DAX) is a fully managed, in-memory cache cluster built only for DynamoDB. It brings cached reads down to microseconds and takes read load off the table.
- It speaks the DynamoDB API. The application swaps the DynamoDB client for the DAX client (Java, .NET, Node.js, Python, Go) and points it at the cluster endpoint;
GetItem,Query,PutItemstay the same. That is what “no application logic change” means. - It runs as a cluster in your VPC: one primary node and up to 10 read replicas; AWS recommends at least three nodes in three AZs for production. You pay per node-hour, so DAX is not a pay-per-request service.
- It is read-through and write-through. A read that misses goes to the table and the result is cached; a write goes through DAX to the table first and then updates the item cache.
#Two caches inside DAX
| Item cache | Query cache | |
|---|---|---|
| Holds | Results of GetItem and BatchGetItem, by primary key | Result sets of Query and Scan, by their exact parameters |
| Default TTL | 5 minutes | 5 minutes |
| Updated by writes through DAX? | Yes (write-through) | No: it keeps the old result set until its TTL expires |
Things DAX does not do, and which the exam likes:
- It caches eventually consistent reads only. A strongly consistent read (or a transactional read) is passed straight to the table, and the result is not cached.
- It does not reduce writes: every write still costs the table’s WCU. For a write flood, DAX is the wrong tool (buffer with SQS, or use on-demand capacity).
- Writes that do not go through DAX (another app writing directly to the table, or a replicated write from another Region of a global table) bypass the cache, which stays stale until the TTL expires.
#DAX or ElastiCache?
Both are in-memory caches, so the exam asks you to choose.
- DAX caches individual items and Query / Scan results of DynamoDB, with no cache code in your application.
- ElastiCache (lesson 7.5) is general purpose: your code decides what to store. Use it for computed or aggregated results (a “most read this week” ranking built from thousands of items), for any database, and for sessions. The application writes and invalidates it itself (cache-aside, write-through, lesson 7.5).
The two often sit side by side: DAX in front of DynamoDB for item reads, ElastiCache for the results the application computes.
DAX: a cache that speaks DynamoDB
A news site stores articles in DynamoDB. During a breaking story the same few articles are read hundreds of thousands of times a minute. DAX sits in the VPC between the app and the table.
Scenario
The first read goes to the table; the next ones come from memory in microseconds.
The app uses the DAX client instead of the plain DynamoDB client: the same get, put and query calls, only the endpoint changes. That is what “no application logic change” means. The first read of the story is a cache miss.
#DynamoDB Streams: react to every change
Many things should happen because data changed, not inside the request that changed it: send a welcome email when a user registers, keep a search index or a summary table up to date, copy changes to an audit log. Doing that inside the sign-up request makes the request slow and fragile.
DynamoDB Streams is an ordered log of item-level changes (insert, modify, remove) on a table.
- Each change produces a stream record, written exactly once, in order per item, and kept for 24 hours.
- The stream view type decides what a record holds:
KEYS_ONLY,NEW_IMAGE,OLD_IMAGEorNEW_AND_OLD_IMAGES(before and after, for audit or diff logic). - The stream is split into shards. No more than two processes should read the same shard at the same time, or they are throttled.
- Enabling a stream does not slow the table, and reading it with Lambda is free of stream read charges.
#Processing the stream
- Lambda (the usual answer): an event source mapping polls the shards and invokes your function with batches of records (lesson 15.1 has the invocation models). Event filtering can drop records before they invoke the function, for example “only
INSERTevents”. - The DynamoDB Streams Kinesis Adapter with the Kinesis Client Library (KCL), for your own consumer application on EC2 or containers.
Typical uses: react in real time (welcome email, push notification), real-time usage analytics, insert into derived tables (a per-day sales summary), cross-Region replication (that is how Global Tables work, below), auditing, and feeding search or analytics stores.
#When a record keeps failing
Records in a shard are processed in order. If the function throws an error on one batch, Lambda retries the same batch again and again, and everything behind it in that shard waits. By default this lasts until the record expires from the stream after 24 hours, and records that expire meanwhile are lost too.
Configure the event source mapping:
- Maximum retry attempts and maximum record age, so a bad batch is given up on.
- Bisect batch on function error: split the batch in two to isolate the bad record.
- An on-failure destination (an SQS queue or SNS topic, or S3) that receives the details of the skipped records, so you can inspect and replay them.
- Or report partial batch failures from the function so only the failed records are retried.
#Kinesis Data Streams for DynamoDB
A table can also send its changes to a Kinesis data stream that you own (lesson 13.5). The slide calls it “newer”; both options exist side by side, and a table can use both at once.
| DynamoDB Streams | Kinesis Data Streams for DynamoDB | |
|---|---|---|
| Retention | 24 hours | Up to 1 year (365 days) |
| Consumers | 2 simultaneous readers per shard | 5 per shard, or 20 with enhanced fan-out |
| Processed with | Lambda triggers, the DynamoDB Streams Kinesis Adapter | Lambda, Data Firehose, Managed Service for Apache Flink, Glue streaming ETL, KCL |
| Duplicates | Never: exactly once | Possible: deduplicate in the consumer |
| Order | Per item, as written | Rebuild from the record’s approximate creation timestamp |
| Shards | Managed by DynamoDB | You size the Kinesis stream (or use on-demand mode) |
The notes put it simply: if you need data kept up to a year, many consumers, or heavier analytics, integrate Kinesis Data Streams; for “do something when an item changes”, DynamoDB Streams + Lambda.
DynamoDB Streams: react to every change
A food delivery app wants a welcome email for every new user, and the analytics team wants every change in S3. Both come from the table’s change log, not from extra code in the sign-up path.
Scenario
Insert → stream record → Lambda polls → email. The sign-up request does not wait for any of it.
Rahim signs up. The API writes one item to the Users table and answers at once. Sending the email inside the request would make sign-up slow and fail whenever the mail service hiccups.
#Global Tables: one table in many Regions
If an app serves users around the world and every one of them needs low-latency reads and writes, a single-Region table is not enough: a write from Dhaka to a table in Virginia crosses the planet. Global Tables keep a full replica of one table in each Region you choose and replicate between them.
- Active-active: every replica accepts reads and writes. That is the difference from an RDS or Aurora read replica, which only reads (lessons 7.1 and 7.3).
- Replication is asynchronous, typically within a second. This default mode is called multi-Region eventual consistency.
- Conflicts: if the same item is changed in two Regions at once, DynamoDB keeps the version with the latest internal timestamp, per item: last writer wins. The other update silently disappears. Conditional writes only check the local replica, so they do not prevent it. Avoid it by design: route all writes for one item (one user, one booking) to one “home” Region.
- Disaster recovery: if a Region fails, send its users to another replica (Route 53 health checks, lesson 8.4); the app keeps reading and writing. Writes that had not replicated yet arrive when the Region recovers, so the recovery point is a few seconds, not zero. Global tables carry a 99.999% availability SLA (single-Region tables: 99.99%).
- Cost: every replicated write is billed as a write in each Region it lands in, plus data transfer between Regions. Three Regions roughly triple the write bill.
- Strongly consistent reads in a Region return the latest local version; an item last written in another Region may be stale there.
“Must enable DynamoDB Streams first.” The notes and the slide state this as a prerequisite, and it is still the answer older exam questions expect. The reason is real: replication reads each replica’s stream. With the current version of Global Tables (2019.11.21), DynamoDB enables the stream for you when you add a replica, and it cannot be switched off on a replica. If an exam option says “enable DynamoDB Streams” next to Global Tables, it is describing that requirement.
#Side note: multi-Region strong consistency (2025)
Since June 2025 a global table can instead be created in multi-Region strong consistency mode:
- Exactly three Regions: three replicas, or two replicas and a witness (a DynamoDB-managed component you cannot read or write).
- A write returns only after it is replicated synchronously to at least one other Region, so a strongly consistent read in any Region returns the latest version, and the recovery point is zero.
- Writes and strong reads are slower (a cross-Region round trip). A write to an item that is being changed in another Region at that moment fails with
ReplicatedWriteConflictExceptionand can be retried, instead of being silently overwritten. - The mode is chosen at creation from an empty table, replicas cannot be added or removed one at a time later, and TTL, LSIs and transactions are not supported.
The exam, unless it says otherwise, means the default mode when it says “Global Tables”.
Global Tables: one table, many Regions
A travel booking app serves Dhaka, Europe and the US. Each Region has a full replica of the bookings table, and users read and write the one nearest to them.
Scenario
Active-active: every replica takes writes, and changes reach the others in about a second.
A Dhaka user books a flight. The write goes to the Singapore replica, a few milliseconds away, and succeeds there. Nobody waits for Europe or the US.
#TTL: let old items delete themselves
Session tokens, OTPs, shopping carts left behind, logs you must keep for 90 days and not a day longer: data with a natural end date. Deleting it with your own job costs write capacity and code. Time to Live (TTL) does it for free.
- Pick an attribute name (for example
expires_at) and enable TTL on the table for it. - Each item stores a Number in that attribute: the expiry time as Unix epoch seconds (for example
1793170200). Items without the attribute, or with a value that is not a number, never expire. - After the time passes, a background process finds and deletes the item, typically within a few days, not at the second. Until then the expired item still shows up in reads, queries and scans: filter it out in the app (
expires_at > now). - TTL deletes consume no write capacity and cost nothing. In a global table, the replicated deletes in the other Regions are billed as normal writes.
- Each TTL delete appears in the table’s stream as a
REMOVErecord made by the service (userIdentity=dynamodb.amazonaws.com), so a Lambda function can archive expired items to S3 before they are gone.
#Backups: PITR and on-demand
A script with a wrong filter, a bad deployment, a mistaken DeleteItem loop: replication does not help with any of these, because the mistake is replicated too. DynamoDB has two backup mechanisms, and neither touches the table’s performance or capacity.
Point-in-time recovery (PITR): continuous backups.
- Once enabled, you can restore to any second within the recovery period. The notes and slide say “the last 35 days”; since January 2025 the period is configurable from 1 to 35 days (35 is the default and the maximum). A shorter window can help with data-retention rules.
- Billed by table size.
On-demand backups: full backups you take when you want (before a migration, at month end).
- Kept until you delete them, for long-term retention and audits.
- No impact on performance or latency, even for huge tables.
- Can be managed by AWS Backup (lesson 23.2): backup plans and schedules, cross-Region and cross-account copies, cold storage for long retention, and vault lock.
Both share one rule the notes underline: a restore always creates a new table. It never overwrites the running table. You then point the application at the new table, or copy the missing items back into the old one. The restored table gets the data, indexes and capacity mode, but not everything: auto scaling policies, IAM policies, CloudWatch alarms, tags, stream settings, TTL settings and the PITR setting must be set again.
#Export to S3 and import from S3
Export to S3 writes a table’s data to an S3 bucket as files.
- Requires PITR, because it reads from the continuous backup, not from the table: it uses no read capacity and does not affect live traffic.
- Full export of the table as it was at any point in the recovery window, or incremental export of only the changes between two times.
- Formats: DynamoDB JSON or Amazon Ion. Can go to a bucket in another account or Region.
- Uses: analytics with Amazon Athena (SQL on the files, lesson 17.1), snapshots for audits, and ETL before importing the data back into DynamoDB.
Import from S3 loads files into DynamoDB.
- Formats: CSV, DynamoDB JSON or Amazon Ion, optionally compressed (
GZIPorZSTD). - Always creates a new table; you cannot import into an existing one.
- Consumes no write capacity, so it is far cheaper than writing millions of items with
PutItem. - Rows that fail validation are logged in CloudWatch Logs.
TTL, backups and S3: the life of the data
A mobile wallet keeps OTP sessions and transactions in DynamoDB. Old sessions must disappear on their own, a bad script must be undoable, and finance wants reports without touching production.
Scenario
Expired items are deleted in the background, free, within a few days, and the deletes appear in the stream.
Session item
session_id
s-77f1
otp_hash
9c1e…
expires_at
1793170200
Each OTP session item gets an attribute expires_at: a Number holding a Unix epoch time in seconds, here 10 minutes after creation. TTL is switched on for that attribute name.
#More integrations worth knowing
- Zero-ETL integrations: DynamoDB can replicate a table into Amazon Redshift (generally available since October 2024) or into Amazon OpenSearch Service (through OpenSearch Ingestion) without you writing a pipeline. Both rely on PITR exports for the initial load and change data afterwards. Use them for SQL analytics over fresh data, or full-text search on DynamoDB items. Redshift and OpenSearch are chapter 17.
- VPC gateway endpoint: a Lambda function or EC2 instance in a private subnet reaches DynamoDB through a free gateway endpoint added to the route table, with no NAT gateway and no internet path (lesson 22.2). Interface endpoints (PrivateLink) also exist, for on-premises access over VPN or Direct Connect. DAX, in contrast, already runs inside your VPC.
- Fine-grained access control: an IAM policy can limit a user to the items whose partition key equals their own identity, with the condition key
dynamodb:LeadingKeysand a policy variable such as${cognito-identity.amazonaws.com:sub}, and to certain attributes withdynamodb:Attributes. This is how a university app lets every student read only their own rows directly from DynamoDB; the Cognito side is lesson 15.7.
{
"Effect": "Allow",
"Action": ["dynamodb:GetItem", "dynamodb:Query", "dynamodb:PutItem"],
"Resource": "arn:aws:dynamodb:ap-southeast-1:111122223333:table/StudentGrades",
"Condition": {
"ForAllValues:StringEquals": {
"dynamodb:LeadingKeys": ["${cognito-identity.amazonaws.com:sub}"]
}
}
}
#What goes wrong
| Symptom | Likely cause | Fix |
|---|---|---|
| Writes to the table throttle although the table has spare WCU | A GSI with too little write capacity (back-pressure), or a hot GSI key | Give GSIs at least the table’s WCU or auto scaling / on-demand; shard low-cardinality GSI keys |
ValidationException: Consistent reads are not supported on global secondary indexes | Strongly consistent read requested on a GSI | Read the GSI eventually consistent, then GetItem the item from the table with a strong read if needed; or an LSI (new table) |
| A just-written item is missing from a GSI query | GSI propagation is asynchronous | Read your own writes from the table by key; tolerate the delay |
ItemCollectionSizeLimitExceededException for one customer | Table has an LSI and that partition key value reached 10 GB | Use a GSI instead; redesign the key; archive old items |
| “We need an LSI on our production table” | LSIs exist only from table creation | GSI; or a new table with the LSI, filled by export and import |
| A query through DAX shows an old value | Query cache is not updated by writes; writes bypassed DAX | Shorter query cache TTL; strongly consistent reads for must-be-fresh data |
| DAX did not reduce the bill | Workload is write-heavy, or reads are strongly consistent | DAX only helps eventually consistent reads; for writes use on-demand or buffering |
| No welcome emails for hours, function errors in logs | A poison record blocks the shard; retries until the 24 h expiry | Max retries / record age, bisect batch, on-failure destination, partial batch responses |
| A booking confirmed in one Region vanished | Concurrent writes in two Regions: last writer wins | Single home Region per item; or the strong consistency mode |
| Expired sessions still returned | TTL deletes in the background, typically within days | Filter on the expiry attribute in reads |
| Restored table has no alarms, no TTL, no stream | Those settings are not restored | Re-create them as part of the runbook |
| Export to S3 fails to start | PITR not enabled on the table | Enable PITR first |
#DynamoDB features in system design
- Access patterns first: list the questions the app asks, then give each a key or an index. A single table with a few well-chosen GSIs often replaces several tables. Keep LSIs for the rare case that needs strong reads on another sort key and fits in 10 GB per key.
- Read-heavy, hot items: DAX in front of the table (product pages, news, configuration), ElastiCache for computed results, CloudFront for whole responses (lesson 11.1).
- Event-driven back ends: the table is the source of truth, its stream drives everything else: emails (SES), search indexes, aggregates, audit trails, cache invalidation. Lesson 15.8’s blog uses exactly this.
- Global apps: Global Tables for active-active, one home Region per item for writes that must not conflict; the strong consistency mode when a zero RPO is worth slower writes.
- Data lifecycle: TTL for natural expiry, a stream function to archive expiring items to S3, S3 lifecycle rules after that (lesson 10.5).
- Protection: PITR on every production table, on-demand backups through AWS Backup with cross-Region copies, deletion protection on the table.
- Analytics without hurting production: export to S3 + Athena for ad hoc questions, zero-ETL to Redshift for dashboards, Kinesis Data Streams + Firehose for a continuous change feed.
#Try it yourself
Create a table with an LSI, add a GSI later, query both, turn on TTL, a stream, PITR, export and restore. Output is shortened; names are examples.
REGION=ap-southeast-1
# 1. Orders table: key user_id + order_date, an LSI on price (only possible now, at creation)
aws dynamodb create-table --table-name Orders --region $REGION \
--attribute-definitions AttributeName=user_id,AttributeType=S \
AttributeName=order_date,AttributeType=S AttributeName=price,AttributeType=N \
--key-schema AttributeName=user_id,KeyType=HASH AttributeName=order_date,KeyType=RANGE \
--local-secondary-indexes '[{"IndexName":"by-price","KeySchema":[
{"AttributeName":"user_id","KeyType":"HASH"},{"AttributeName":"price","KeyType":"RANGE"}],
"Projection":{"ProjectionType":"INCLUDE","NonKeyAttributes":["status"]}}]' \
--billing-mode PAY_PER_REQUEST
# "TableStatus": "CREATING", "LocalSecondaryIndexes": [{ "IndexName": "by-price", ... }]
# 2. Months later: a GSI on tracking_no (backfilled while the table stays online)
aws dynamodb update-table --table-name Orders \
--attribute-definitions AttributeName=tracking_no,AttributeType=S \
--global-secondary-index-updates '[{"Create":{"IndexName":"by-tracking",
"KeySchema":[{"AttributeName":"tracking_no","KeyType":"HASH"}],
"Projection":{"ProjectionType":"KEYS_ONLY"}}}]'
aws dynamodb describe-table --table-name Orders \
--query 'Table.GlobalSecondaryIndexes[0].[IndexStatus,Backfilling]'
# [ "CREATING", true ]
# 3. Most expensive first (LSI, strongly consistent allowed)
aws dynamodb query --table-name Orders --index-name by-price \
--key-condition-expression "user_id = :u" \
--expression-attribute-values '{":u":{"S":"user_101"}}' \
--no-scan-index-forward --consistent-read
# "Items": [ { "price": {"N": "45000"}, "order_date": {"S": "2026-05-18"}, ... } ], "Count": 4
# 4. Find by tracking number (GSI). Asking for a strong read here fails:
aws dynamodb query --table-name Orders --index-name by-tracking \
--key-condition-expression "tracking_no = :t" \
--expression-attribute-values '{":t":{"S":"trk-2202"}}' --consistent-read
# An error occurred (ValidationException) when calling the Query operation:
# Consistent reads are not supported on global secondary indexes
# (run it again without --consistent-read)
# 5. TTL on expires_at (Unix epoch seconds, a Number)
aws dynamodb update-time-to-live --table-name Orders \
--time-to-live-specification "Enabled=true, AttributeName=expires_at"
# { "TimeToLiveSpecification": { "Enabled": true, "AttributeName": "expires_at" } }
# 6. A stream with old and new images, and a Lambda trigger that only sees inserts
aws dynamodb update-table --table-name Orders \
--stream-specification StreamEnabled=true,StreamViewType=NEW_AND_OLD_IMAGES
# "LatestStreamArn": "arn:aws:dynamodb:ap-southeast-1:111122223333:table/Orders/stream/2026-10-28T09:12:44.123"
aws lambda create-event-source-mapping --function-name order-welcome-example \
--event-source-arn arn:aws:dynamodb:ap-southeast-1:111122223333:table/Orders/stream/2026-10-28T09:12:44.123 \
--starting-position LATEST --batch-size 100 \
--filter-criteria '{"Filters":[{"Pattern":"{\"eventName\":[\"INSERT\"]}"}]}' \
--maximum-retry-attempts 3 --bisect-batch-on-function-error \
--destination-config '{"OnFailure":{"Destination":"arn:aws:sqs:ap-southeast-1:111122223333:orders-failed-example"}}'
# 7. PITR with a 14-day window, then restore to a second before the accident
aws dynamodb update-continuous-backups --table-name Orders \
--point-in-time-recovery-specification PointInTimeRecoveryEnabled=true,RecoveryPeriodInDays=14
# "PointInTimeRecoveryStatus": "ENABLED", "RecoveryPeriodInDays": 14
aws dynamodb restore-table-to-point-in-time --source-table-name Orders \
--target-table-name Orders-restored --restore-date-time 2026-10-28T14:05:09Z
# "TableStatus": "CREATING" (a new table; re-add alarms, TTL, stream)
# 8. An on-demand backup for the year-end audit
aws dynamodb create-backup --table-name Orders --backup-name orders-2026-year-end
# "BackupStatus": "CREATING"
# 9. Export to S3 for Athena: no read capacity used
aws dynamodb export-table-to-point-in-time \
--table-arn arn:aws:dynamodb:ap-southeast-1:111122223333:table/Orders \
--s3-bucket ddb-exports-example --export-format DYNAMODB_JSON
# "ExportStatus": "IN_PROGRESS"
# 10. Make it a global table: add a replica in Frankfurt (stream enabled for you)
aws dynamodb update-table --table-name Orders \
--replica-updates '[{"Create":{"RegionName":"eu-central-1"}}]'
aws dynamodb describe-table --table-name Orders --query 'Table.Replicas'
# [ { "RegionName": "eu-central-1", "ReplicaStatus": "CREATING" } ]
A DAX cluster is one more command (aws dax create-cluster --cluster-name orders-cache --node-type dax.r5.large --replication-factor 3 --iam-role-arn … --subnet-group-name …), after which the app only changes its client and endpoint.
#Summary
Key takeaways
- LSI: same partition key, different sort key; only at table creation, never deleted alone; shares the table’s capacity; strong and eventual reads; 10 GB per item collection; 5 per table.
- GSI: any attribute as partition key (sort key optional); add or delete any time (backfilled); own capacity; eventual reads only; no size limit; 20 per table by default. Under-provisioned GSI write capacity throttles the base table.
- Projections (
KEYS_ONLY,INCLUDE,ALL) decide index size and write cost. Never Scan when an index can answer. - DAX: in-memory, write-through cache cluster in your VPC, DynamoDB API, microseconds, no logic change; item and query caches, 5-minute default TTL; caches eventually consistent reads only; does not reduce writes. ElastiCache for computed or aggregated results and other databases.
- DynamoDB Streams: ordered item-level changes, exactly once, 24 hours, 4 view types, Lambda or the KCL adapter; set retries, bisect and an on-failure destination. Kinesis Data Streams for DynamoDB: up to 1 year, more consumers, possible duplicates.
- Global Tables: multi-Region active-active, async replication in about a second, last writer wins, 99.999% SLA; Streams underneath (enabled for you today, the exam still says “enable Streams”). Strong consistency mode (2025): three Regions, synchronous, RPO zero, no TTL / LSI / transactions.
- TTL: epoch-seconds Number attribute, background delete within days, free, visible in Streams; filter expired items in reads.
- PITR: any second in 1 to 35 days (configurable since 2025); on-demand backups until deleted, managed by AWS Backup for cross-Region / cross-account copies. Every restore creates a new table.
- Export to S3 (needs PITR, no RCU, full or incremental, JSON or Ion) + Athena; import from S3 (CSV, JSON, Ion) creates a new table, no WCU, errors in CloudWatch Logs.
- Also: zero-ETL to Redshift / OpenSearch, a free gateway endpoint from private subnets,
dynamodb:LeadingKeysfor per-user rows.
Answer on your own first, then read the explanation, even when you got it right.
Self-checkQuestion
An Orders table has partition key user_id and sort key order_date. Which statement about a local secondary index on it is correct?
Self-checkQuestion
Which index type supports strongly consistent reads?
Self-checkQuestion
What is the default TTL of items in the DAX item cache?
Self-checkQuestion
How long does DynamoDB Streams keep a stream record?
Self-checkQuestion
An application has run for eight months. The team now needs to look up orders by email address, which is not part of the primary key. What should they do?
Self-checkQuestion
A table runs in provisioned mode with 2,000 WCU. A GSI on order status has 200 WCU. During a sale, writes to the table start failing with ProvisionedThroughputExceededException although the table’s consumed WCU is only 900. What is the cause?
Self-checkQuestion
A gaming company stores player profiles in DynamoDB with partition key player_id. Each player has a few KB of data. The app needs, for each player, their match history sorted by score, with strongly consistent reads, and the table is being designed now. Which is the best choice?
Self-checkQuestion
A logistics company plans an LSI on a table keyed by customer_id. Some large customers will accumulate about 30 GB of shipment records each. What happens?
Self-checkQuestion
Which two statements about global secondary indexes are correct?
Choose 2
Self-checkQuestion
A support screen needs only an order’s tracking number, status and total from a GSI. The table items are 6 KB, mostly product descriptions. Which projection keeps the GSI cheapest while answering the screen in one query?
Self-checkQuestion
An online news site reads the same few hundred DynamoDB items millions of times an hour, with eventually consistent reads. The team wants microsecond reads and must not rewrite the data access code. What should they use?
Self-checkQuestion
A dashboard computes the top 10 products of the week from 200,000 DynamoDB items. The result should be cached for 10 minutes. Which service fits best?
Self-checkQuestion
Through DAX, an app runs a Query for the politics section, then updates one politics story through DAX. A second identical Query a minute later shows the old headline. Why?
Self-checkQuestion
Which two statements about DAX are correct?
Choose 2
Self-checkQuestion
A table has 10,000 eventually consistent reads per second of 4 KB items. After adding DAX, 95% of reads are cache hits. Roughly how many RCU does the table now need for these reads?
Self-checkQuestion
When a new user registers, a sign-up API writes an item to a Users table. The company wants a welcome email sent for every new user without slowing down the sign-up request. Which design fits?
Self-checkQuestion
A Lambda function processes a DynamoDB stream. One malformed record makes the function fail, and no other records from that shard are processed for hours. Which two changes fix this?
Choose 2
Self-checkQuestion
The analytics team wants to keep DynamoDB change records for 30 days and feed them to Firehose, a Flink application and three other consumers. What should be enabled?
Self-checkQuestion
Which statement correctly contrasts DynamoDB Streams with Kinesis Data Streams for DynamoDB?
Self-checkQuestion
A travel company needs its DynamoDB bookings table writable with low latency in Singapore, Frankfurt and Virginia, and the app must keep working if one Region fails. What should the architect use?
Self-checkQuestion
In a global table in the default mode, the same item is updated in Singapore and in Frankfurt within the same second. What happens?
Self-checkQuestion
An exam question says: Before creating a DynamoDB global table, what must be enabled on the table? Which answer does it expect, and what is true today?
Self-checkQuestion
A bank wants a DynamoDB table in three Regions where a strongly consistent read in any Region always returns the latest committed write, and no acknowledged write may be lost in a Regional outage. It accepts slower writes. Which option fits?
Self-checkQuestion
A global table has replicas in three Regions. The app writes 200 items per second, each 1 KB, in one Region. Ignoring indexes, how many write capacity units’ worth of writes does this cause in total across all Regions each second?
Self-checkQuestion
A session item should expire 10 minutes after it is created at Unix time 1793169600. What should its TTL attribute hold, and when will it be deleted?
Self-checkQuestion
Which two statements about DynamoDB TTL are correct?
Choose 2
Self-checkQuestion
At 14:05:10 a faulty script deleted 40,000 items from a production table with PITR enabled. What is the correct recovery?
Self-checkQuestion
The notes and the slide say PITR restores to any point in the last 35 days. What is accurate today?
Self-checkQuestion
Finance wants to run SQL over a year of DynamoDB orders every month without affecting the production table’s performance. Which approach is best?
Self-checkQuestion
Which two statements about DynamoDB import from S3 are correct?
Choose 2
Self-checkQuestion
A university app lets students call DynamoDB directly with temporary credentials from a Cognito identity pool. Each student must read only the items whose partition key is their own identity ID. How is this enforced?