Course outline

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 key order_date.
  • “User_101’s orders, newest first” is one Query on the table (ScanIndexForward=false reads 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 keySame as the table (user_id)Any attribute (tracking_no, email)
Sort keyDifferent from the table (price)Optional, any attribute (order_date)
When createdOnly at table creationAny time, at creation or later (backfilled)
When deletedOnly by deleting the tableAny time
CapacityShares the table’s RCU / WCUIts own RCU / WCU
Size limit10 GB per item collection (one partition key value, table + LSIs)None
Read consistencyStrong and eventualEventual only
How many5 per table20 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 example status and price for 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.

Same partition
Shop app
LSI: by priceuser_id + price
Orders tableuser_id + order_date
GSI: by trackingtracking_no
Query user_101

Orders table, partition user_101

user_idorder_dateprice
user_1012026-03-021,200
user_1012026-05-1845,000
user_1012026-08-093,500
Step 1 of 4
1

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, PutItem stay 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 cacheQuery cache
HoldsResults of GetItem and BatchGetItem, by primary keyResult sets of Query and Scan, by their exact parameters
Default TTL5 minutes5 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.

VPC
Article APIDAX client
DAX cluster3 nodesmiss
Articles table
get a-42
Step 1 of 4
1

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_IMAGE or NEW_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 INSERT events”.
  • 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 StreamsKinesis Data Streams for DynamoDB
Retention24 hoursUp to 1 year (365 days)
Consumers2 simultaneous readers per shard5 per shard, or 20 with enhanced fan-out
Processed withLambda triggers, the DynamoDB Streams Kinesis AdapterLambda, Data Firehose, Managed Service for Apache Flink, Glue streaming ETL, KCL
DuplicatesNever: exactly oncePossible: deduplicate in the consumer
OrderPer item, as writtenRebuild from the record’s approximate creation timestamp
ShardsManaged by DynamoDBYou 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.

Sign-up API
Users table
Table stream24 h
Welcome function
Email (SES)
On-failure queue
put user
Step 1 of 4
1

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 ReplicatedWriteConflictException and 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.

ap-southeast-1
eu-central-1
us-east-1
Dhaka users
Europe users
US users
Replica
Replica
Replica
put booking
Step 1 of 4
1

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 REMOVE record 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 (GZIP or ZSTD).
  • 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.

Wallet app
Wallet table
Stream function
S3 bucket
Athena
PITR backups
Restored table
put session

Session item

session_id

s-77f1

otp_hash

9c1e…

expires_at

1793170200

Step 1 of 4
1

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:LeadingKeys and a policy variable such as ${cognito-identity.amazonaws.com:sub}, and to certain attributes with dynamodb: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

SymptomLikely causeFix
Writes to the table throttle although the table has spare WCUA GSI with too little write capacity (back-pressure), or a hot GSI keyGive 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 indexesStrongly consistent read requested on a GSIRead 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 queryGSI propagation is asynchronousRead your own writes from the table by key; tolerate the delay
ItemCollectionSizeLimitExceededException for one customerTable has an LSI and that partition key value reached 10 GBUse a GSI instead; redesign the key; archive old items
“We need an LSI on our production table”LSIs exist only from table creationGSI; or a new table with the LSI, filled by export and import
A query through DAX shows an old valueQuery cache is not updated by writes; writes bypassed DAXShorter query cache TTL; strongly consistent reads for must-be-fresh data
DAX did not reduce the billWorkload is write-heavy, or reads are strongly consistentDAX only helps eventually consistent reads; for writes use on-demand or buffering
No welcome emails for hours, function errors in logsA poison record blocks the shard; retries until the 24 h expiryMax retries / record age, bisect batch, on-failure destination, partial batch responses
A booking confirmed in one Region vanishedConcurrent writes in two Regions: last writer winsSingle home Region per item; or the strong consistency mode
Expired sessions still returnedTTL deletes in the background, typically within daysFilter on the expiry attribute in reads
Restored table has no alarms, no TTL, no streamThose settings are not restoredRe-create them as part of the runbook
Export to S3 fails to startPITR not enabled on the tableEnable 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:LeadingKeys for per-user rows.