Reading & Writing Data
The operations available in DynamoDB, their cost implications, consistency models, and the condition expressions that make writes safe.
Table of Contents
Reading: GetItem & BatchGetItem
GetItem is the fastest and cheapest read operation — a single item lookup by its complete primary key. If you know the exact key, always use GetItem over Query.
GetItem
GetItem Characteristics
- ✅Fetch a single item by its complete primary key (PK + SK if composite)
- ✅Strongly consistent or eventually consistent (ConsistentRead parameter)
- ✅ProjectionExpression: return only specific attributes — reduces bandwidth, NOT RCU
- ✅RCU cost: ceil(itemSize / 4 KB) for strong, half for eventual
- ✅Returns empty if item doesn't exist (no error thrown)
// GetItem — exact primary key lookup const result = await dynamodb.getItem({ TableName: "Orders", Key: { PK: { S: "USER#123" }, SK: { S: "ORDER#2024-01-15#abc" } }, ConsistentRead: true, // strongly consistent (2× RCU) ProjectionExpression: "orderId, #s, total", // only these attributes ExpressionAttributeNames: { "#s": "status" } // 'status' is reserved word });
BatchGetItem
BatchGetItem Characteristics
- ✅Fetch up to 100 items or 16 MB in one API call
- ✅Items can be from multiple tables
- ✅Internally parallel — DynamoDB fetches in parallel
- ✅Not a transaction — each GetItem is independent
- ✅Partial failure: UnprocessedKeys returned — retry with exponential backoff
Reading: Query
Query fetches items sharing a partition key, optionally filtered by sort key conditions. It is the primary tool for fetching collections of related items.
// Query — fetch all orders for a user in January 2024 const result = await dynamodb.query({ TableName: "Orders", KeyConditionExpression: "PK = :pk AND SK BETWEEN :start AND :end", ExpressionAttributeValues: { ":pk": { S: "USER#123" }, ":start": { S: "ORDER#2024-01-01" }, ":end": { S: "ORDER#2024-01-31" } }, ScanIndexForward: false, // reverse sort (newest first) Limit: 10, // max 10 items to evaluate FilterExpression: "#s = :status", // applied AFTER fetch ExpressionAttributeNames: { "#s": "status" }, ExpressionAttributeValues: { ...prev, ":status": { S: "DELIVERED" } } });
FilterExpression Does NOT Reduce Cost
FilterExpression is applied AFTER items are read from the partition. You pay RCU for ALL items that match the KeyConditionExpression, even if FilterExpression discards 99% of them. If you're filtering heavily, your key design is wrong.
Query Mechanics
Query Mechanics
- ✅KeyConditionExpression: PK = value [AND SK condition] — required
- ✅SK conditions: =, <, <=, >, >=, BETWEEN, begins_with
- ✅Returns items sorted by sort key ascending by default
- ✅ScanIndexForward: false — reverse sort order
- ✅Limit: max items to EVALUATE (not items to return — confusing!)
- ✅Pagination: LastEvaluatedKey → pass as ExclusiveStartKey in next call
- ✅Can query base table or any GSI/LSI
Key Condition
DynamoDB finds the partition, applies sort key condition to narrow items
Read Items
All matching items read from partition (RCU charged here)
Filter Expression
Non-key filters applied — items removed from response but already charged
Limit Check
If Limit reached during evaluation, stop and return LastEvaluatedKey
Return
Filtered items returned to client with pagination token if more exist
Reading: Scan
Scan reads every item in a table or index. No key condition — it reads everything. This is almost always the wrong operation for production code paths.
| Aspect | Query | Scan |
|---|---|---|
| Key condition | Required (PK + optional SK) | None — reads everything |
| Cost | Only items in partition | Entire table charged |
| Performance | Predictable, fast | Unpredictable, slow at scale |
| Use case | Production queries | Migrations, analytics, exports |
| Parallelism | Not needed | Parallel scan (Segment/TotalSegments) |
When Scan is Acceptable
When Scan is Acceptable
- ✅One-time data migrations (not recurring)
- ✅Small tables (< 1000 items) where cost is negligible
- ✅Background analytics jobs (off-peak, rate-limited)
- ✅Table exports to S3 (use DynamoDB Export instead)
- ✅Development/debugging (never in production hot paths)
Parallel Scan
Parallel scan divides the table into N segments, scanning each in parallel. Faster but consumes N× the RCU simultaneously. Use for large one-time migrations, never for user-facing requests.
Consistency Models
| Model | Behavior | RCU Cost | Availability |
|---|---|---|---|
| Eventually Consistent (default) | May return stale data (usually < 1 second old) | Half cost (0.5 RCU per 4 KB) | Base table + GSI + Global Tables |
| Strongly Consistent | Always returns latest committed data | Full cost (1 RCU per 4 KB) | Base table + LSI only |
| Transactional Read | Snapshot isolation across multiple items | 2× cost (2 RCU per 4 KB) | Base table only, same region |
The News Wire Analogy
Eventually consistent reads are like checking a news aggregator — the story might be 30 seconds behind the wire service. Strongly consistent reads are like calling the reporter directly — you always get the latest. Transactional reads are like getting a synchronized snapshot of multiple reporters at the exact same moment. Each level costs more but gives stronger guarantees.
When Strong Consistency Matters
Use strongly consistent reads for: financial balances, inventory counts, anything where stale data means incorrect behavior. Use eventually consistent (default) for: user profiles, product catalogs, anything where a 1-second delay is acceptable.
Writing: PutItem & UpdateItem
PutItem — Create or Full Replace
PutItem Characteristics
- ✅Writes an item — creates new or FULLY REPLACES existing
- ✅Entire item replaced if key exists — not a merge, not a partial update
- ✅ConditionExpression: only write if condition is true (optimistic concurrency)
- ✅ReturnValues: ALL_OLD returns the previous item before replacement
- ✅WCU cost: ceil(max(oldItemSize, newItemSize) / 1 KB)
UpdateItem — Partial Modification
// UpdateItem — atomic increment + set attribute const result = await dynamodb.updateItem({ TableName: "Products", Key: { PK: { S: "PRODUCT#123" }, SK: { S: "PRODUCT#123" } }, UpdateExpression: "SET #views = #views + :one, #name = :newName REMOVE #deprecated", ConditionExpression: "attribute_exists(PK)", // only update existing items ExpressionAttributeNames: { "#views": "viewCount", "#name": "name", "#deprecated": "oldField" }, ExpressionAttributeValues: { ":one": { N: "1" }, ":newName": { S: "Updated Product" } }, ReturnValues: "UPDATED_NEW" // return new values of updated attributes });
Update Expression Actions
| Action | Purpose | Example |
|---|---|---|
| SET | Add or overwrite attributes | SET #count = #count + :val |
| REMOVE | Delete attributes from item | REMOVE #oldField, #temp |
| ADD | Add to number or add elements to set | ADD #tags :newTags |
| DELETE | Remove elements from a set | DELETE #tags :removeTags |
Upsert Behavior
UpdateItem creates the item if it doesn't exist (upsert). To prevent this, add ConditionExpression: attribute_exists(PK). To ensure create-only, use PutItem with ConditionExpression: attribute_not_exists(PK).
Writing: DeleteItem & BatchWriteItem
DeleteItem
DeleteItem Characteristics
- ✅Remove a single item by its complete primary key
- ✅ConditionExpression: only delete if condition met
- ✅ReturnValues: ALL_OLD returns the deleted item
- ✅No error if item doesn't exist (unless condition fails)
- ✅WCU cost: ceil(itemSize / 1 KB) — charged even for non-existent items with conditions
BatchWriteItem
BatchWriteItem Characteristics
- ✅Write or delete up to 25 items or 16 MB in one API call
- ✅Mix of PutItem and DeleteItem operations (no UpdateItem!)
- ✅Items can span multiple tables
- ✅NOT atomic — partial success possible, UnprocessedItems returned
- ✅No condition expressions in batch — only unconditional puts/deletes
- ✅Use for: bulk loading, bulk deletion, data migrations
No UpdateItem in Batch
BatchWriteItem only supports PutItem and DeleteItem. If you need batch updates, you must use individual UpdateItem calls or TransactWriteItems (limited to 25 items, but supports updates).
Condition Expressions
Condition expressions make any write operation conditional — atomic compare-and-swap without transactions. They are the foundation of safe concurrent writes in DynamoDB.
Common Condition Expression Patterns: ═══════════════════════════════════════════════════════════════ Pattern | Use Case ═══════════════════════════════════════════════════════════════ attribute_not_exists(PK) | Only create, fail if exists attribute_exists(PK) | Only update existing items #version = :expected_version | Optimistic locking #status = :active | Only process active items #balance >= :amount | Sufficient funds check size(#items) < :maxItems | Bounded collections ═══════════════════════════════════════════════════════════════ Failed condition → ConditionalCheckFailedException This is NOT an error — it's expected flow. Handle in application.
Why Conditions Matter
Without Conditions
- Two users update same item simultaneously
- Last write wins silently
- First user's changes lost without notice
- No way to detect the conflict
With Conditions
- Both users include version condition
- First write succeeds, increments version
- Second write fails (version mismatch)
- Application retries with fresh data
Interview Questions
Q:What is the difference between Query and Scan?
A: Query requires a partition key and optionally filters by sort key — it reads only items in one partition. Scan reads every item in the entire table. Query cost is proportional to items in the partition; Scan cost is proportional to the entire table size. Use Query for production, Scan only for migrations/exports.
Q:Why does FilterExpression not reduce RCU cost?
A: FilterExpression is applied AFTER items are read from the partition. DynamoDB first reads all items matching the KeyConditionExpression (charging RCU), then filters in memory. If you Query 1000 items and FilterExpression keeps 10, you still pay for 1000 items of RCU. The solution is better key design, not more filters.
Q:What is the difference between PutItem and UpdateItem?
A: PutItem creates or FULLY REPLACES an item — all existing attributes are overwritten. UpdateItem modifies specific attributes while preserving others. UpdateItem also supports atomic operations (increment counters, append to lists) and creates the item if it doesn't exist (upsert). Use PutItem for full writes, UpdateItem for partial modifications.
Q:How does optimistic locking work in DynamoDB?
A: Add a 'version' attribute to items. On update, include ConditionExpression: #version = :expectedVersion and increment version in the UpdateExpression. If another writer modified the item (version changed), the condition fails with ConditionalCheckFailedException. The application retries: read fresh item, apply changes, attempt update again.
Q:What happens when a Query returns LastEvaluatedKey?
A: It means more items exist that match the query but weren't returned (either due to 1 MB response limit or Limit parameter). Pass LastEvaluatedKey as ExclusiveStartKey in the next Query call to continue pagination. When LastEvaluatedKey is absent from the response, all matching items have been returned.
Common Mistakes
Using Scan where Query would work
Scanning the entire table then filtering in application code. If you're filtering by a known attribute, create a GSI with that attribute as the partition key. A Query on a GSI is orders of magnitude cheaper than a Scan with FilterExpression.
✅Create a GSI with the filter attribute as partition key and use Query instead of Scan.
Assuming FilterExpression saves money
Adding FilterExpression to reduce results without realizing you still pay for all items read. If your Query returns 10,000 items and FilterExpression keeps 50, you pay for 10,000 items. Redesign your sort key or add a GSI instead.
✅Redesign your sort key to include the filter attribute, or create a GSI that supports the access pattern directly.
Using PutItem when UpdateItem is needed
PutItem replaces the entire item. If you only want to update one attribute but use PutItem without including all other attributes, you'll lose data. Use UpdateItem for partial modifications.
✅Use UpdateItem with SET expressions for partial modifications to preserve existing attributes.
Not handling ConditionalCheckFailedException
Condition expressions are expected to fail in concurrent systems. Not catching and retrying ConditionalCheckFailedException means your application silently drops writes. Implement retry logic with exponential backoff.
✅Catch ConditionalCheckFailedException, re-read the item, and retry the operation with exponential backoff and jitter.
Confusing Limit with result count
Limit controls how many items DynamoDB EVALUATES, not how many it returns. With Limit=10 and a FilterExpression that matches 20% of items, you might get only 2 results. You must paginate until LastEvaluatedKey is absent to get all matching items.
✅Always paginate using LastEvaluatedKey until it is absent from the response to retrieve all matching items.