DynamoDB and AWS Data Stores for Developers (DVA-C02)
Using data stores in application development means choosing the right AWS database or storage service for each access pattern, then reading and writing it efficiently from code — and on the DVA-C02 exam that revolves around Amazon DynamoDB. Task 1.3 sits in Domain 1 (Development with AWS Services, 32% of scored content) and probes whether you can design high-cardinality partition keys, pick between a GSI and an LSI, explain why Query beats Scan, and choose strongly versus eventually consistent reads. It also expects you to place data correctly across the wider storage landscape: S3 for objects, EBS and EFS for block and file workloads, RDS and Aurora when you genuinely need SQL, ElastiCache and DAX for caching, and specialized stores such as Amazon OpenSearch Service and MemoryDB for access patterns a key-value table serves badly. This lesson works through each decision the way exam questions frame it: a scenario, a constraint, and one best answer.
On this page8 sections
- DynamoDB primary keys and partition design
- Secondary indexes: GSI versus LSI
- Query versus Scan
- Consistency models and capacity units
- Serializing and deserializing data for persistence
- Choosing the data store: relational, DynamoDB, or specialized
- Object, file, block, and ephemeral storage
- Caching strategies in application code
- Design high-cardinality DynamoDB partition keys that spread traffic evenly and avoid hot partitions
- Choose between global and local secondary indexes based on key shape, consistency, and creation-time constraints
- Compare Query and Scan operations and predict their capacity cost and performance impact
- Select the right AWS data store — DynamoDB, RDS or Aurora, S3, EFS, EBS, OpenSearch Service, or MemoryDB — for a given access pattern
- Implement caching with ElastiCache or DAX using lazy loading, write-through, and TTL strategies
- Manage data lifecycles automatically with DynamoDB TTL and S3 lifecycle rules
DynamoDB primary keys and partition design
Every DynamoDB table is defined by its primary key, and the partition key half of that key decides how work spreads across the table's underlying storage partitions. DynamoDB hashes the partition key value to place each item, so a key with many distinct, evenly requested values — a high-cardinality key — balances reads and writes across partitions. A low-cardinality key (a status flag, a date, a handful of tenant ids) funnels traffic into a hot partition that throttles even though the table as a whole has spare capacity. Adaptive capacity softens the blow by shifting throughput toward busy partitions, but it cannot rescue a fundamentally skewed key. Design the key first.
You choose between two primary key shapes: a simple key (partition key only, every item addressed by one value) and a composite key (partition key plus sort key, where the partition key groups related items and the sort key orders them within the group). The composite shape is what makes Query powerful: one partition, a range of sort keys.
Walk through a multi-tenant orders table. Requirements: fetch one order by id, and list a tenant's recent orders. If you make tenantId the partition key, one large tenant concentrates traffic on one partition. A better design uses orderId — unique and high-cardinality — as the partition key so single-order GetItem calls balance perfectly, plus a global secondary index with partition key tenantId and sort key createdAt for the tenant listing. If one tenant still dominates, apply write sharding: suffix the index partition key (tenantId#0 through tenantId#9) and fan queries out across the shards.
One hard limit to memorize: an item, including all attribute names and values, cannot exceed 400 KB. Store large payloads in Amazon S3 and keep a pointer in the item.
Secondary indexes: GSI versus LSI
Secondary indexes give a table alternate access paths without duplicating it by hand. A global secondary index (GSI) defines a completely new partition key (and optional sort key) and behaves like a shadow table with its own throughput. A local secondary index (LSI) keeps the table's partition key and swaps in a different sort key, staying local to each partition.
| Dimension | GSI | LSI |
|---|---|---|
| Key schema | Any partition key, optional sort key | Same partition key, alternate sort key |
| When created | Any time, even on an existing table | Only at table creation — cannot be added later |
| Read consistency | Eventually consistent only | Strongly or eventually consistent |
| Capacity | Own read/write capacity | Shares the table's capacity |
| Size limit | None | 10 GB per partition key value (item collection) |
Projection decides which attributes the index copies: KEYS_ONLY, INCLUDE (named attributes), or ALL. Project only what your queries read — a slim GSI costs less to write and store, but querying an attribute that is not projected forces an expensive fetch back to the base table.
Choosing between them is a staple exam question. Reach for a GSI when you need to query on a different partition key entirely (orders by customer instead of by order id) or when the table already exists. Reach for an LSI only when you need an alternate sort order within the same partition and strongly consistent reads on that index — and remember you must decide at table creation. A useful pattern either way is the sparse index: only items that carry the index key are copied in, so indexing an attribute that exists solely on, say, unshipped orders yields a small, cheap index of exactly those items.
Query versus Scan
Query and Scan both return sets of items, but they work in opposite ways. Query requires an equality condition on the partition key, optionally narrowed by a sort key condition (equals, less-than, greater-than, between, begins_with), and reads only the matching items in that one partition. Scan reads every item in the table or index, page by page, and evaluates any filter afterward.
The cost difference is the exam's favorite angle. A filter expression does not reduce consumed capacity: filtering happens after items are read, so a Scan with a filter that returns three items still pays read capacity for every item scanned. Both operations return at most 1 MB of data per call and hand back a LastEvaluatedKey for pagination, so a whole-table answer also means many round trips.
When a question asks for the approach with the least read capacity, lowest latency, or lowest cost, the answer is to restructure access around Query — usually by adding a GSI that turns the filter attribute into a partition key — or to use GetItem and BatchGetItem for known keys. Scan is legitimate for genuine whole-table work: occasional exports, migrations, analytics sweeps. For those, a parallel scan (the Segment and TotalSegments parameters) splits the table across workers, and using a smaller page size or eventually consistent reads softens the impact on live traffic.
Distractor alert: an option that adds a filter expression to reduce cost is wrong by definition, and a projection expression reduces the network payload but not the consumed capacity either.
Consistency models and capacity units
DynamoDB replicates every item across multiple Availability Zones, and its read consistency model follows from that. An eventually consistent read (the default) may hit a replica that has not yet received the latest write, so it can briefly return stale data. A strongly consistent read returns the most recent successful write, at the price of higher latency, no support on global secondary indexes, and double the read capacity. You opt in per request by setting ConsistentRead to true on GetItem, Query, or Scan — it applies to base tables and LSIs only. When several items must change together, TransactWriteItems and TransactGetItems provide ACID guarantees at twice the capacity cost of standard operations.
Capacity math shows up in developer-level questions, and the units are stable facts worth memorizing. One read capacity unit (RCU) is one strongly consistent read per second of an item up to 4 KB — or two eventually consistent reads of that size. One write capacity unit (WCU) is one write per second of an item up to 1 KB. Larger items round up: a strongly consistent read of a 6 KB item costs 2 RCUs, while an eventually consistent read of it costs 1.
You also choose a throughput mode. Provisioned capacity sets RCUs and WCUs ahead of time (optionally with auto scaling) and throttles with ProvisionedThroughputExceededException when exceeded — the SDKs retry with exponential backoff automatically. On-demand mode bills per request and absorbs spiky traffic without capacity planning, which makes it the exam's answer for new, unknown, or bursty workloads.
Serializing and deserializing data for persistence
Persisting application objects means converting them to the target store's wire format and back. DynamoDB stores items as typed attributes, not raw JSON: strings (S), numbers (N), binary (B), booleans (BOOL), null, lists (L), maps (M), and string, number, and binary sets (SS, NS, BS). In the low-level API every value arrives as an explicitly typed structure — and numbers travel as strings, which preserves precision across programming languages. Binary values are base64-encoded in transit.
You rarely hand-build that structure. Every SDK ships a document-style client or marshaller — the JavaScript DynamoDBDocumentClient, Python's resource-level Table, Java's Enhanced Client — that converts native objects (dicts, POJOs, plain JavaScript objects) to DynamoDB attribute types on write and back to native types on read. Nested JSON maps cleanly onto M and L attributes, so a document that would be one opaque JSON blob elsewhere becomes queryable structure in DynamoDB. The exam expects you to know that the SDK, not your application code, owns this translation.
Two design consequences follow. First, serialize only what you query or must retrieve together: item size drives both the 400 KB ceiling and per-request capacity, so a compact item holding an S3 pointer beats an item embedding a large payload. Second, choose attribute types deliberately — storing a timestamp as N (epoch seconds) enables sort key range conditions and DynamoDB TTL, while an ISO-8601 string sorts correctly as S and stays human-readable. Either works; mixing the two across items breaks range queries.
Choosing the data store: relational, DynamoDB, or specialized
Choosing between relational and non-relational stores is an access-pattern decision, not a popularity contest. Amazon RDS and Amazon Aurora give you full SQL: ad hoc queries, multi-table joins, complex transactions, and compatibility with existing ORM-based codebases. They fit when query patterns are unknown up front or genuinely relational — reporting, normalized schemas, aggregations across entities. The developer-level trade-offs: connection management matters (many concurrent Lambda invocations can exhaust a connection pool, the problem RDS Proxy exists to solve), and scaling writes means scaling the instance. DynamoDB inverts the model: you design the table around known access patterns, and in exchange you get single-digit-millisecond latency at any scale, no connection pooling, native IAM authentication, and per-request pricing. Exam questions signal DynamoDB with phrases like key-value access, massive scale, serverless, or unpredictable traffic — and signal RDS or Aurora with joins, complex queries, or an existing SQL application.
Skill 1.3.9 adds specialized stores chosen by access pattern. Amazon OpenSearch Service is the answer for full-text search, relevance ranking, fuzzy matching, and log analytics — patterns DynamoDB serves badly because they cannot be expressed as key lookups. A common architecture streams DynamoDB changes into OpenSearch so the table remains the system of record while OpenSearch answers search queries. Amazon MemoryDB is a Redis-compatible in-memory database with multi-AZ durability via a distributed transaction log: unlike a cache, it can serve as the primary database when you need microsecond reads on data you cannot afford to lose. Amazon Athena rounds out the pattern list — SQL directly over data sitting in S3, for analytics without loading a database at all.
Object, file, block, and ephemeral storage
Placing data on the right storage service is the other half of this task. Amazon S3 is object storage: HTTP-addressed, effectively unlimited, the default home for user uploads, static assets, exports, and any payload too large for a database item. Amazon EBS is block storage — a network-attached disk bound to an EC2 instance in a single Availability Zone, right for self-managed databases or anything expecting low-latency block access. Amazon EFS is a shared NFS filesystem that many EC2 instances, containers, or Lambda functions can mount simultaneously — the answer when multiple compute nodes must read and write the same files.
Distinguish persistent from ephemeral storage. Lambda's /tmp directory (512 MB by default, configurable up to 10 GB) survives only as long as the execution environment does — use it as scratch space, never as a store of record. EC2 instance store volumes are physically attached and fast, but their data is lost when the instance stops or terminates. Anything that must outlive the compute belongs in S3, EBS, EFS, or a database.
S3 storage classes and data lifecycles
S3 storage classes trade retrieval speed for cost: Standard for hot data, Intelligent-Tiering when access patterns are unknown (it moves objects between tiers automatically), Standard-IA and One Zone-IA for infrequently accessed data, and the Glacier classes for archives. Lifecycle rules automate the journey — transition objects to cheaper classes after a set age, expire them entirely, or clean up incomplete multipart uploads. On the DynamoDB side, TTL does the lifecycle job: designate a number attribute holding an epoch-seconds timestamp, and a background process deletes expired items at no write cost. Deletion is not instantaneous — expired items can linger briefly and still appear in reads, so filter them out in code if exactness matters; TTL deletes also flow into DynamoDB Streams for downstream cleanup.
Caching strategies in application code
A cache sits between your code and the data store to cut latency and read load, and the exam tests the strategies more than the services. With lazy loading (cache-aside), code checks the cache first and, on a miss, reads the database and writes the result into the cache. Only requested data is cached, but every miss pays a penalty and entries can go stale. With write-through, code updates the cache on every database write: reads are always fresh, but you cache data nobody may read and every write does double work. Real systems combine both and attach a TTL to every entry so stale or orphaned data ages out; explicit invalidation (deleting keys on update) tightens freshness where TTLs are too blunt. A read-through cache moves the miss-handling out of your code entirely — the cache itself fetches from the store, which is exactly the model DAX implements.
| Dimension | ElastiCache for Redis | ElastiCache for Memcached | DAX |
|---|---|---|---|
| Works with | Any data source | Any data source | DynamoDB only |
| Features | Rich data structures, pub/sub, replication, snapshots, Multi-AZ failover | Simple key-value, multithreaded, no persistence or replication | Write-through item cache and query cache |
| Pick when | Leaderboards (sorted sets), session stores, high availability | Simplest possible object cache that scales out | Microsecond DynamoDB reads without cache code |
DAX is the trigger-word answer when a question pairs DynamoDB with microsecond latency or asks to accelerate reads without modifying application logic — its client is API-compatible with the DynamoDB SDK and caches GetItem and BatchGetItem results (item cache) plus Query and Scan results (query cache). Choose ElastiCache instead when caching anything other than DynamoDB — RDS query results, session state, computed aggregates — or when you need Redis data structures. Boundary note: caching whole HTTP responses at the edge is CloudFront's job, and encrypting cached or stored data belongs to Domain 2.
Tip. Task 1.3 questions are scenario-driven: a table that throttles under load (hot partition — answer with a high-cardinality key or write sharding), a lookup that must use the least read capacity (Query against a GSI, never Scan with a filter), or a read that must always return the latest write (strongly consistent — which rules out a GSI). Watch the trigger words: microsecond latency with DynamoDB means DAX; full-text search or log analytics means OpenSearch Service; joins or complex queries mean RDS or Aurora; unpredictable traffic means on-demand capacity. Expect at least one question on GSI versus LSI constraints and one on caching trade-offs, where lazy loading risks stale reads and write-through pays a penalty on every write.
- Choose a high-cardinality partition key — a low-cardinality key creates hot partitions that no capacity setting can fix.
- LSI: same partition key, alternate sort key, table-creation only, supports strongly consistent reads. GSI: any keys, created anytime, eventually consistent only.
- Query reads only matching items; Scan reads the whole table and consumes capacity for every item scanned — filter expressions never reduce cost.
- Strongly consistent reads work on tables and LSIs, cost twice the RCUs, and are never available on a GSI.
- DynamoDB items max out at 400 KB — store large payloads in S3 and keep a pointer in the item.
- Lazy loading caches only what is read but risks staleness; write-through stays fresh but doubles write work — combine them with TTLs.
- DAX is the answer for microsecond DynamoDB reads with no cache logic; ElastiCache is the general-purpose cache for everything else.
- Match store to access pattern: S3 for objects, EBS for block, EFS for shared files, OpenSearch Service for full-text search and log analytics, MemoryDB for durable in-memory data.
Frequently asked questions
What is a hot partition in DynamoDB and how do I avoid it?
A hot partition occurs when a disproportionate share of reads or writes targets one partition key value, causing throttling on that partition even though the table has unused capacity overall. Avoid it by choosing a high-cardinality partition key whose values are requested evenly — an order id rather than a status flag or a date — and, when one key value is unavoidably popular, by write sharding: appending a suffix such as #0 through #9 to spread the load, then fanning queries across the shards. DynamoDB's adaptive capacity helps absorb temporary skew but cannot compensate for a fundamentally unbalanced key design.
What is the difference between a GSI and an LSI in DynamoDB?
A global secondary index (GSI) defines a completely new partition key and optional sort key, can be created or deleted at any time, has its own provisioned throughput, and supports only eventually consistent reads. A local secondary index (LSI) must use the table's partition key with an alternate sort key, can only be created when the table is created, shares the table's throughput, supports strongly consistent reads, and caps each item collection at 10 GB per partition key value. In practice most designs use GSIs; choose an LSI only when you need an alternate sort order within a partition together with strong consistency.
When should I use DAX instead of ElastiCache?
Use DAX when the data being cached lives in DynamoDB and you want microsecond read latency without writing any caching logic — the DAX client is API-compatible with the DynamoDB SDK, so it drops in as a read-through, write-through layer that caches GetItem, BatchGetItem, Query, and Scan results. Use ElastiCache (Redis or Memcached) when you are caching anything else — RDS query results, session state, computed aggregates — or when you need Redis features such as sorted sets, pub/sub, or replication with failover. DAX only ever fronts DynamoDB; ElastiCache is source-agnostic.
Does a DynamoDB Scan with a filter expression cost less than one without?
No. Filter expressions are applied after DynamoDB reads the items, so a Scan consumes read capacity for every item it examines regardless of how few items the filter lets through. A filter reduces the data returned over the network, not the capacity consumed. To genuinely reduce read cost, restructure the access as a Query — typically by creating a global secondary index whose partition key is the attribute you were filtering on — or fetch known keys directly with GetItem or BatchGetItem.
When should I choose RDS or Aurora over DynamoDB?
Choose RDS or Aurora when your application needs full SQL: multi-table joins, ad hoc queries whose patterns are not known in advance, complex transactions across entities, or compatibility with an existing relational schema and ORM. Choose DynamoDB when access patterns are known and key-based, traffic is large or unpredictable, and you want single-digit-millisecond latency with no connection management — a natural fit for serverless architectures, since thousands of concurrent Lambda invocations would exhaust a relational connection pool. On the exam, joins and complex queries point to RDS or Aurora; scale, key-value access, and serverless point to DynamoDB.
What is Amazon MemoryDB and how is it different from a cache?
Amazon MemoryDB is a Redis-compatible, in-memory database designed to be a primary data store, not a cache in front of one. It persists every write to a distributed transaction log across multiple Availability Zones, so data survives node failures — unlike ElastiCache, where cached data is treated as disposable and the source of truth lives elsewhere. Choose MemoryDB when an access pattern demands microsecond reads and Redis data structures on data you cannot afford to lose; choose ElastiCache when you are accelerating reads against another database that remains the system of record.
Sign up free to mark lessons complete, bookmark topics and track your exam readiness.