What Is DynamoDB?
Amazon DynamoDB is a fully managed, serverless NoSQL database service from AWS. It stores data as items in tables and uses primary keys to distribute and retrieve those items efficiently. Unlike traditional relational databases, DynamoDB does not require applications to manage database servers, storage volumes, replication infrastructure, or routine capacity provisioning.
DynamoDB is designed to deliver single-digit millisecond performance for suitable access patterns, even as datasets and traffic grow substantially. It is widely used for user profiles, shopping carts, session data, metadata, orders, IoT workloads, and event-driven applications. However, getting good performance requires more than creating a table. Partition key design, access patterns, secondary indexes, consistency, and handling hot keys are central to building reliable DynamoDB systems.
Table of Contents
- What Amazon DynamoDB Provides
- How DynamoDB Works
- DynamoDB Data Modeling
- Consistency, Concurrency, and Transactions
- Scaling, Pricing, and Data Management
- Production Design Example: Multi-Tenant Order Service
- Ready-to-Use Example: DynamoDB with Terraform and Python
- Performance and Production Best Practices
- Frequently Asked Questions
- Conclusion
What Amazon DynamoDB Provides
DynamoDB is a distributed database designed around efficient primary-key access. You create tables, define their keys and optional indexes, and make API requests to read or modify items. AWS handles the underlying storage infrastructure, replication across Availability Zones, and scaling mechanisms.
The main engineering trade-off is that DynamoDB gives applications predictable and scalable access when queries align with the data model. In exchange, developers must make more decisions about access patterns upfront than they typically would with a relational database.
Common Use Cases
DynamoDB is a strong fit for workloads with known access patterns, high request volumes, and relatively simple operations on individual items or related groups of items.
- User profiles: Retrieve and update account information by user ID.
- Shopping carts: Store and update cart contents by customer or session.
- Order metadata: Retrieve orders by tenant, customer, or order identifier.
- Sessions and tokens: Store temporary records with automatic expiration.
- IoT: Store device configuration and recent device state.
- Event processing: Track job status, deduplication records, and checkpoints.
- Multi-tenant applications: Isolate logical item collections with carefully designed tenant keys.
DynamoDB is less suitable when an application relies heavily on arbitrary joins, complex aggregations, or ad hoc filtering across large datasets. Those workloads may need a relational database, a dedicated analytics system, or additional indexing infrastructure.
DynamoDB vs. Relational Databases
Both DynamoDB and Amazon RDS can store business data, but their query models and scaling characteristics differ substantially.
| Characteristic | DynamoDB | Amazon RDS |
|---|---|---|
| Database model | Key-value and document NoSQL | Relational SQL |
| Primary access | Key-based reads and queries | SQL queries and indexes |
| Joins | No native relational joins | Supported |
| Infrastructure | Fully managed, serverless | Managed database instances or engine-specific options |
| Scaling | Distributed keyspace and managed throughput | Instance scaling, replicas, and other engine-specific methods |
| Schema | Flexible item attributes | Defined relational schema |
| Typical strengths | High-scale predictable access | Complex queries and relational transactions |
DynamoDB supports conditional updates and ACID transactions, so it is inaccurate to describe it as a database with no transaction guarantees. The important differences are its query model, indexing approach, and the operational costs of performing access patterns not supported by its keys.
How DynamoDB Works
A DynamoDB table contains items rather than relational rows with a fixed set of columns. Items are identified by a primary key, and their non-key attributes can differ from one item to another.
Internally, DynamoDB distributes data using partition keys. Developers do not manually assign partitions to physical servers, but their choice of partition key strongly affects how reads and writes are distributed.
Tables, Items, and Attributes
A table can store multiple item shapes. For example, an orders table might contain:
{
"pk": "TENANT#42",
"sk": "ORDER#9f2a",
"order_id": "9f2a",
"customer_id": "customer-17",
"status": "PENDING",
"total_cents": 4999,
"created_at": "2026-10-18T14:30:00Z",
"version": 1
}
DynamoDB supports strings, numbers, booleans, binary values, lists, maps, sets, and null attributes. A standard DynamoDB item has a maximum size of 400 KB, including attribute names and values.
Large documents, videos, and images usually belong in S3, with their location and searchable metadata stored in DynamoDB.
Partition Keys and Sort Keys
A DynamoDB table can use either a simple primary key or a composite primary key.
- Simple primary key: One partition key uniquely identifies an item.
- Composite primary key: A partition key and sort key together uniquely identify an item.
With a composite key, multiple items can have the same partition key as long as their sort keys differ.
pk sk
----------- -----------------------
TENANT#42 ORDER#1001
TENANT#42 ORDER#1002
TENANT#42 ORDER#1003
TENANT#99 ORDER#2001
TENANT#99 ORDER#2002
The application can query all orders for TENANT#42 without specifying an exact sort key. It can also narrow the results using supported sort-key conditions, such as begins_with or a range comparison.
This is a major advantage of composite keys: related items can be accessed through a shared partition key without individually fetching every object.
Query, Scan, and Secondary Indexes
DynamoDB provides several important read operations, each intended for a different access pattern.
- GetItem: Retrieve an item using its complete primary key.
- Query: Retrieve items with a specific partition key, optionally restricting sort-key values.
- Scan: Read across a table or index without using a partition-key equality condition.
- BatchGetItem: Retrieve multiple items identified by their keys.
A Query requires a partition-key equality condition. If the table also has a sort key, specifying a sort-key condition is optional.
import boto3
from boto3.dynamodb.conditions import Key
table = boto3.resource("dynamodb").Table("orders")
# Retrieves orders in a tenant's item collection.
# No explicit sort-key condition is required.
response = table.query(
KeyConditionExpression=Key("pk").eq("TENANT#42")
)
orders = response["Items"]
# Query responses can be paginated.
while "LastEvaluatedKey" in response:
response = table.query(
KeyConditionExpression=Key("pk").eq("TENANT#42"),
ExclusiveStartKey=response["LastEvaluatedKey"],
)
orders.extend(response["Items"])
Secondary indexes support additional access patterns. A global secondary index (GSI) can use different partition and sort keys from the base table, allowing applications to efficiently query items by another attribute combination.
A local secondary index (LSI) keeps the table's partition key but uses an alternative sort key. LSIs must be defined when the table is created, while GSIs can be added later.
Be careful with FilterExpression. Filtering happens after DynamoDB reads the candidate items, so it does not reduce the read capacity consumed by the initial key-based read. Likewise, a GSI is not free: it adds storage and write costs.
DynamoDB Data Modeling
DynamoDB design usually starts with the application's questions rather than its entities. In a relational database, it is natural to create normalized tables first and write different queries afterward. In DynamoDB, doing that often leads to expensive scans or a growing collection of secondary indexes.
The goal is to make the application's most important operations efficient, predictable, and compatible with the chosen primary keys.
Start with Access Patterns
Suppose an application needs to manage orders for thousands of business tenants. Before choosing keys, identify how requests will retrieve data.
| Access Pattern | Recommended Access | Key Design |
|---|---|---|
| Get an order by tenant and order ID | GetItem | pk = TENANT#id, sk = ORDER#id |
| List a tenant's orders | Query | Partition key = TENANT#id |
| List pending orders for a tenant | GSI Query | gsi1pk = TENANT#id#STATUS#PENDING |
| List recent pending orders | GSI Query | gsi1sk = timestamp#order-id |
Each access pattern should have a deliberate path to its data. A common warning sign is a critical endpoint whose only implementation is scanning the entire table and filtering in application code.
Sorting behavior also matters. DynamoDB sorts items by sort key, using numeric ordering for numeric keys and byte-based lexicographical ordering for string keys. ISO 8601 UTC timestamps are useful string sort-key components because they preserve chronological order when formatted consistently.
Single-Table Design and Item Collections
Single-table design stores multiple related entity types in one DynamoDB table. For example, a tenant profile, orders, and customer records might share a table but use different sort-key prefixes.
pk sk
------------ -------------------------
TENANT#42 PROFILE
TENANT#42 ORDER#1001
TENANT#42 ORDER#1002
TENANT#42 CUSTOMER#17
TENANT#99 PROFILE
TENANT#99 ORDER#2001
This approach can support efficient item-collection queries and transactional updates across related records. However, storing everything in one table does not automatically improve performance.
Single-table design introduces coupling between access patterns, index design, and item naming conventions. Separate tables are entirely valid when workloads have different security requirements, retention policies, throughput needs, or independent ownership boundaries.
For a concrete application model, see DynamoDB Data Modeling: Blog Platform Example, which discusses author feeds, post metadata, and index design in a blog platform.
Hot Partitions and Uneven Traffic
A poorly chosen partition key can concentrate a large share of traffic on a narrow portion of the keyspace. This is commonly called a hot partition or hot key problem.
Imagine storing every active order under pk = ORDERS. The table may contain millions of items, but writes for all tenants target the same logical partition-key value.
Using tenant-specific keys distributes activity across tenants, although one extremely busy tenant can still dominate traffic. In that case, the application may need additional key sharding, time buckets, or another access strategy.
Do not assume that total table capacity guarantees unlimited throughput for one key. Adaptive capacity helps with uneven traffic, but physical partition limits and sustained hot access patterns still matter.
Related techniques are covered in Database Sharding Strategies and Trade-Offs.
Consistency, Concurrency, and Transactions
DynamoDB's distributed architecture requires explicit choices about read consistency and concurrent modification. A successful write is durably stored, but not every supported read path guarantees immediate visibility of that update.
The correct choice depends on whether temporary staleness is harmless or could violate business rules.
Eventually Consistent vs. Strongly Consistent Reads
Eventually consistent reads are the default. They may briefly return an older committed value after a successful write.
Strongly consistent reads can be requested for supported operations against base tables and local secondary indexes. They reflect successful prior writes, but GSIs and DynamoDB Streams do not support strongly consistent reads.
- Eventually consistent: Suitable for feeds, reporting, and many noncritical listings.
- Strongly consistent: Useful for immediate verification of account state, inventory, or other correctness-sensitive reads from a supported table or LSI.
- GSI reads: Always eventually consistent, regardless of the base table's consistency capabilities.
For example, immediately querying a GSI after changing an order's status may still show the previous index state. If the user needs to confirm the order update, read the base-table item using a strongly consistent read.
See Consistency Models in Distributed Systems for a broader explanation of consistency guarantees.
Conditional Writes and Optimistic Locking
Conditional writes let an application update an item only when a specified condition holds. DynamoDB evaluates the condition atomically as part of the write operation.
For example, two workers might both read an order at version 3 and try to mark it as processed. A version condition ensures that only the first valid update succeeds.
from botocore.exceptions import ClientError
def change_order_status(table, tenant_id, order_id):
key = {
"pk": f"TENANT#{tenant_id}",
"sk": f"ORDER#{order_id}",
}
try:
response = table.update_item(
Key=key,
UpdateExpression=(
"SET #status = :processed, "
"#version = #version + :one"
),
ConditionExpression=(
"attribute_exists(pk) AND "
"#version = :expected AND "
"#status = :pending"
),
ExpressionAttributeNames={
"#status": "status",
"#version": "version",
},
ExpressionAttributeValues={
":processed": "PROCESSED",
":pending": "PENDING",
":expected": 3,
":one": 1,
},
ReturnValues="ALL_NEW",
)
return response["Attributes"]
except ClientError as exc:
if exc.response["Error"]["Code"] == (
"ConditionalCheckFailedException"
):
# Another worker may have updated the order.
return None
raise
The expected version must come from the previously read item. A hardcoded version is shown here only to illustrate the condition.
Conditional writes can prevent lost updates without introducing a separate distributed lock. This is often simpler and more reliable when the conflict concerns one DynamoDB item.
ACID Transactions
DynamoDB supports transactions for operations that must succeed or fail together across multiple items, including items in different tables within the same supported transaction scope.
For example, creating an order and reserving inventory might require both writes to succeed. A transactional request can update the inventory item and create the order atomically.
Transactions have item-count, total-size, capacity, and other service constraints. They are also generally more expensive than equivalent nontransactional operations.
Use a transaction when the business invariant genuinely spans multiple items. For a single-item state change, a conditional update is usually sufficient and simpler.
Scaling, Pricing, and Data Management
DynamoDB removes server provisioning, but it does not remove throughput limits or cost considerations. Efficient capacity usage still depends on item sizes, access patterns, indexes, and the amount of data read or written.
Operational features such as Time to Live, Streams, point-in-time recovery, and global tables help support data lifecycle and distributed workflows.
On-Demand vs. Provisioned Capacity
DynamoDB offers two main table capacity modes.
| Mode | Billing | Typical Workload |
|---|---|---|
| On-demand | Read and write request units consumed | Variable traffic and unpredictable demand |
| Provisioned | Configured read/write capacity over time | Predictable, sustained workloads |
On-demand capacity automatically adapts to demand and is a convenient starting point for many applications. However, new traffic peaks, table-level limits, configurable maximum throughput, and hot-key behavior can still cause throttling.
Provisioned capacity can be economical when request rates are stable and predictable. Auto Scaling can adjust provisioned capacity based on utilization, but capacity planning remains relevant.
DynamoDB read and write charges are based partly on item size and consistency. For example, eventually consistent reads consume less read capacity than equivalent strongly consistent reads. GSIs also consume capacity and storage.
TTL, Streams, and Backups
DynamoDB Time to Live (TTL) marks items for eventual automatic deletion based on a numeric Unix timestamp attribute. TTL is useful for sessions, temporary tokens, deduplication records, and expiring workflow metadata.
TTL deletion is asynchronous. An expired item can remain readable until it is actually deleted, so applications must check expiration themselves when immediate rejection is required.
DynamoDB Streams records item changes and allows consumers such as AWS Lambda to react asynchronously. Streams are useful for audit pipelines, projections, search indexing, and downstream notifications.
Streams consumers must tolerate duplicate processing and retry scenarios. Do not assume every update triggers exactly one successful business side effect.
DynamoDB also supports on-demand backups and point-in-time recovery (PITR). Recovery generally creates a restored table, so applications need a plan for routing traffic to the restored data and reconciling changes.
For related event processing ideas, read What Is Change Data Capture?.
Global Tables and Multi-Region Replication
DynamoDB global tables replicate data across multiple AWS Regions and allow applications to access geographically distributed replicas.
Global tables support different consistency configurations, including multi-Region eventual consistency and a multi-Region strong consistency option with specific feature and configuration limitations.
For eventually consistent global tables, writes in one Region are asynchronously replicated to others. Concurrent changes to the same item across Regions require understanding how replication conflicts are resolved.
Cross-Region applications should not assume that a transaction committed in one Region becomes atomically visible in another Region under eventual replication. A multi-Region architecture needs explicit consistency, conflict, failure-routing, and recovery policies.
Production Design Example: Multi-Tenant Order Service
Consider a SaaS application that processes orders for thousands of merchants. Each merchant needs to retrieve its own orders, list pending orders, and update order status while preventing concurrent workers from processing the same order twice.
The application runs a FastAPI service behind an API Gateway or load balancer and uses DynamoDB as the operational database. Order events are consumed asynchronously by background workers.
Orders, Status Queries, and Tenant Isolation
A practical table design uses these attributes:
Base table:
pk = TENANT#{tenant_id}
sk = ORDER#{order_id}
Status index (GSI1):
gsi1pk = TENANT#{tenant_id}#STATUS#{status}
gsi1sk = {created_at}#{order_id}
Example:
pk = TENANT#42
sk = ORDER#9f2a
gsi1pk = TENANT#42#STATUS#PENDING
gsi1sk = 2026-10-18T14:30:00Z#9f2a
This design supports direct order reads, listing all orders for a tenant, and querying pending orders for a particular tenant. It does not require scanning every merchant's orders to find pending work.
When an order status changes from PENDING to PROCESSED, the application updates the item's status index attributes as part of the same item update. The GSI reflects the change asynchronously.
Tenant isolation is not achieved by key naming alone. The API must authorize the tenant identity associated with every operation, and IAM conditions can provide additional restrictions in suitable direct-access designs.
Failure Scenarios and Idempotency
Suppose two background workers receive the same order-processing event. Both read the order while its status is PENDING, but only one should transition it to PROCESSED.
A conditional update checks the expected status and version. The successful worker advances the version; the second worker receives a conditional check failure and should stop or re-read the current state.
Another common failure occurs when DynamoDB successfully commits an order but the API response is lost. The client retries and may attempt to create the order again.
Use a stable client-generated request ID or order ID with a conditional create. Repeated requests should retrieve the existing operation and confirm that its original payload matches the retry. For more complex operations, combine idempotency records with DynamoDB transactions.
Do not publish critical external side effects before ensuring the database state transition is durable. A stream-based workflow or another reliable handoff mechanism can help coordinate downstream processing.
Ready-to-Use Example: DynamoDB with Terraform and Python
This example provisions a DynamoDB table with a composite primary key, a global secondary index, DynamoDB Streams, point-in-time recovery, and TTL. A Python repository demonstrates conditional creation, strongly consistent reads, pagination, status queries, and optimistic concurrency.
You need Terraform, Python, an AWS account, and AWS credentials with the necessary DynamoDB permissions. The example uses on-demand capacity to avoid configuring throughput manually.
1. Project Setup
Create a directory with the following files:
dynamodb-demo/
main.tf
outputs.tf
requirements.txt
app.py
Authenticate with an IAM role, AWS CLI profile, or another supported credential provider. Avoid hardcoding AWS access keys in application code or Terraform variables.
2. Create the DynamoDB Table
Create main.tf:
terraform {
required_version = ">= 1.5.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.0"
}
}
}
provider "aws" {
region = "us-west-2"
}
resource "aws_dynamodb_table" "orders" {
name = "sysdespro-orders-demo"
billing_mode = "PAY_PER_REQUEST"
# Composite table primary key.
hash_key = "pk"
range_key = "sk"
attribute {
name = "pk"
type = "S"
}
attribute {
name = "sk"
type = "S"
}
# Global secondary index key attributes.
attribute {
name = "gsi1pk"
type = "S"
}
attribute {
name = "gsi1sk"
type = "S"
}
global_secondary_index {
name = "status-index"
hash_key = "gsi1pk"
range_key = "gsi1sk"
projection_type = "ALL"
}
# Capture item changes for event-driven consumers.
stream_enabled = true
stream_view_type = "NEW_AND_OLD_IMAGES"
# Restore data to a selected point within
# the supported recovery window.
point_in_time_recovery {
enabled = true
}
# Expiration attribute for temporary records.
ttl {
attribute_name = "expires_at"
enabled = true
}
server_side_encryption {
enabled = true
}
tags = {
Environment = "development"
ManagedBy = "terraform"
}
}
The GSI uses projection_type = "ALL" to simplify the demo. In production, evaluate whether a narrower projection would reduce index storage and write costs.
TTL is enabled for items that contain expires_at. The order items in this example do not set that attribute, so they will not automatically expire.
Create outputs.tf:
output "table_name" {
description = "DynamoDB orders table."
value = aws_dynamodb_table.orders.name
}
output "table_arn" {
description = "DynamoDB table ARN."
value = aws_dynamodb_table.orders.arn
}
output "stream_arn" {
description = "DynamoDB Streams ARN."
value = aws_dynamodb_table.orders.stream_arn
}
3. Implement Repository Operations with boto3
Create requirements.txt:
boto3
Create app.py:
import os
import uuid
from datetime import datetime, timezone
from decimal import Decimal
import boto3
from boto3.dynamodb.conditions import Key
from botocore.exceptions import ClientError
TABLE_NAME = os.environ["DYNAMODB_TABLE"]
table = boto3.resource(
"dynamodb",
region_name=os.getenv("AWS_REGION", "us-west-2"),
).Table(TABLE_NAME)
def create_order(
tenant_id: str,
order_id: str,
total_cents: int,
) -> dict:
if total_cents <= 0:
raise ValueError("Amount must be positive")
created_at = datetime.now(timezone.utc).isoformat()
item = {
"pk": f"TENANT#{tenant_id}",
"sk": f"ORDER#{order_id}",
"order_id": order_id,
"tenant_id": tenant_id,
"total_cents": total_cents,
"status": "PENDING",
"version": 1,
"created_at": created_at,
"gsi1pk": f"TENANT#{tenant_id}#STATUS#PENDING",
"gsi1sk": f"{created_at}#{order_id}",
}
try:
table.put_item(
Item=item,
ConditionExpression=(
"attribute_not_exists(pk)"
),
)
return item
except ClientError as exc:
if exc.response["Error"]["Code"] != (
"ConditionalCheckFailedException"
):
raise
# A repeated creation request must not
# overwrite an existing order.
raise ValueError(
"Order already exists"
) from exc
def get_order(tenant_id: str, order_id: str):
response = table.get_item(
Key={
"pk": f"TENANT#{tenant_id}",
"sk": f"ORDER#{order_id}",
},
ConsistentRead=True,
)
return response.get("Item")
def list_orders(tenant_id: str) -> list[dict]:
orders = []
start_key = None
while True:
params = {
"KeyConditionExpression": (
Key("pk").eq(f"TENANT#{tenant_id}")
& Key("sk").begins_with("ORDER#")
)
}
if start_key:
params["ExclusiveStartKey"] = start_key
response = table.query(**params)
orders.extend(response["Items"])
start_key = response.get("LastEvaluatedKey")
if not start_key:
break
return orders
def list_pending_orders(tenant_id: str):
# GSIs support eventually consistent reads only.
response = table.query(
IndexName="status-index",
KeyConditionExpression=Key("gsi1pk").eq(
f"TENANT#{tenant_id}#STATUS#PENDING"
),
)
return response["Items"]
def mark_processed(
tenant_id: str,
order_id: str,
expected_version: int,
):
try:
response = table.update_item(
Key={
"pk": f"TENANT#{tenant_id}",
"sk": f"ORDER#{order_id}",
},
UpdateExpression=(
"SET #status = :processed, "
"#version = #version + :one, "
"gsi1pk = :index_key"
),
ConditionExpression=(
"attribute_exists(pk) AND "
"#version = :expected AND "
"#status = :pending"
),
ExpressionAttributeNames={
"#status": "status",
"#version": "version",
},
ExpressionAttributeValues={
":processed": "PROCESSED",
":pending": "PENDING",
":one": 1,
":expected": expected_version,
":index_key": (
f"TENANT#{tenant_id}#STATUS#PROCESSED"
),
},
ReturnValues="ALL_NEW",
)
return response["Attributes"]
except ClientError as exc:
if exc.response["Error"]["Code"] == (
"ConditionalCheckFailedException"
):
return None
raise
def main():
tenant_id = "42"
order_id = uuid.uuid4().hex
created = create_order(
tenant_id=tenant_id,
order_id=order_id,
total_cents=4999,
)
print("Created:", created["order_id"])
# Strongly consistent read from the base table.
order = get_order(tenant_id, order_id)
print("Current status:", order["status"])
# Atomically update the status and version.
processed = mark_processed(
tenant_id=tenant_id,
order_id=order_id,
expected_version=order["version"],
)
assert processed is not None
print("Updated:", processed["status"])
# A stale version cannot apply the same update.
repeated = mark_processed(
tenant_id=tenant_id,
order_id=order_id,
expected_version=order["version"],
)
assert repeated is None
print("Duplicate update prevented")
print("Tenant orders:", len(list_orders(tenant_id)))
if __name__ == "__main__":
main()
The code uses conditional item creation and version checks to prevent unintended overwrites. It also updates the GSI partition-key attribute when the order's status changes, keeping the index aligned with the current order state after asynchronous propagation.
The listing methods deliberately illustrate two different behaviors: list_orders() handles pagination, while list_pending_orders() returns only the first query page. A production GSI query should expose a pagination token or iterate through additional pages as required.
Application authorization must verify that callers can access the requested tenant_id. Passing a tenant ID from the client directly into these repository methods is not, by itself, sufficient authorization.
4. Deploy and Test
Provision the table and run the Python program:
# Initialize Terraform and validate configuration.
terraform init
terraform fmt
terraform validate
# Review the planned resources.
terraform plan
# Create the DynamoDB table.
terraform apply
# Configure the Python client.
export AWS_REGION="us-west-2"
export DYNAMODB_TABLE="$(terraform output -raw table_name)"
# Install dependencies.
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
# Create and update a sample order.
python app.py
# Inspect table metadata.
aws dynamodb describe-table \
--table-name "$DYNAMODB_TABLE" \
--query "Table.{Status:TableStatus,ItemCount:ItemCount,Billing:BillingModeSummary}"
# Remove the disposable resources.
terraform destroy
The script creates one order, reads it with strong consistency, transitions it to PROCESSED, and verifies that a repeated update using the old version fails safely.
The example deletes the table during cleanup. Do not use the same Terraform destruction workflow against a production table containing customer data. Production tables should have deletion protection, backup policies, restricted administrative permissions, and tested recovery procedures.
Performance and Production Best Practices
DynamoDB can handle enormous workloads, but table-level scaling does not compensate for inefficient access patterns. Most serious production issues involve hot keys, scans, unbounded item collections, costly indexes, or incorrect assumptions about consistency and retries.
A well-designed system measures the behavior of its most important keys and queries rather than only watching total request volume.
Monitoring and Troubleshooting
Monitor DynamoDB through CloudWatch and application-level metrics. Useful signals include:
- Consumed read/write capacity: Actual throughput usage.
- Throttled requests: Operations limited by capacity or partition-level constraints.
- Successful request latency: Service-side response latency.
- System errors: Service-side failures requiring retry or investigation.
- Conditional check failures: Expected concurrency conflicts or unexpected contention.
- GSI capacity and throttling: Secondary index pressure.
- Replication latency: Cross-Region propagation delay where applicable.
- Stream consumer lag: Delay before change events are processed.
Use retries with exponential backoff and jitter for transient failures. Do not blindly retry conditional failures that represent real business conflicts.
For high-throughput workloads, consider DynamoDB Contributor Insights and application-level key statistics to identify keys that receive disproportionately high traffic.
Common Mistakes
Common DynamoDB design and implementation mistakes include:
- Designing tables before queries: Important endpoints require full-table scans.
- Using low-cardinality partition keys: Many requests concentrate on a small number of logical keys.
- Using FilterExpression as an index: Items are read before filtering, increasing cost.
- Ignoring pagination: Query or Scan results are incomplete after the first response.
- Assuming GSIs are strongly consistent: Recent updates may not appear immediately.
- Using floating-point values for money: Decimal precision and serialization become unsafe.
- Blindly overwriting items: Concurrent requests lose changes without conditions.
- Treating TTL as immediate deletion: Expired records may remain until background cleanup.
- Storing large binary objects: Items approach the 400 KB limit and increase request costs.
- Ignoring secondary-index costs: Write amplification increases with indexed attributes.
- Skipping idempotency for retries: Duplicate events create duplicate business side effects.
DynamoDB performs well when developers embrace its access model instead of forcing it to behave like a general-purpose SQL database.
Production Checklist
Before using DynamoDB for an important application, verify that:
- All critical access patterns have efficient key-based operations.
- Partition keys distribute expected traffic without severe hot spots.
- Composite sort keys support required ordering and filtering.
- GSIs are necessary, correctly projected, and cost-conscious.
- Every paginated operation handles continuation keys.
- Strong consistency is requested where supported and required.
- Concurrent updates use conditions, transactions, or suitable coordination.
- Retries and stream consumers are idempotent.
- TTL expiration is validated by the application when immediate rejection matters.
- Backups and point-in-time recovery match recovery requirements.
- IAM permissions and tenant isolation are enforced.
- Capacity consumption, throttling, latency, and key distribution are monitored.
Frequently Asked Questions
DynamoDB differs from relational databases in how data is organized and accessed, but it still provides strong consistency options, transactions, indexing, and managed durability features.
What Is Amazon DynamoDB Used For?
Amazon DynamoDB is used for operational workloads requiring scalable, low-latency reads and writes. Typical examples include profiles, shopping carts, orders, session records, application metadata, device state, and distributed workflow coordination.
Does DynamoDB Support SQL?
DynamoDB is not a traditional SQL database and does not support relational joins. It does support PartiQL, a SQL-compatible query language for supported DynamoDB operations, but the underlying access patterns, indexes, and capacity costs still apply.
Can DynamoDB Query Using Only a Partition Key?
Yes. For a table with both a partition key and sort key, a Query requires the partition-key condition, while a sort-key condition is optional.
GetItem, however, requires the complete primary key, including the sort key when one exists.
Is DynamoDB Strongly Consistent?
DynamoDB supports strongly consistent reads for base tables and local secondary indexes. Eventually consistent reads are the default, and global secondary indexes always use eventual consistency.
Global tables introduce additional cross-Region consistency considerations, depending on their configured mode.
Is DynamoDB Really Serverless?
Yes. DynamoDB is fully managed and serverless from the application's perspective. AWS manages the underlying infrastructure, replication, and storage. Developers configure data models, access policies, indexes, and capacity modes rather than database servers.
Is DynamoDB Free?
DynamoDB has applicable AWS Free Tier allowances or credits for eligible accounts and workloads. Outside those allowances, charges depend on the capacity mode, reads, writes, storage, backups, indexes, global replication, and optional features.
On-demand mode avoids charging for idle provisioned throughput, but stored data and other enabled features can still incur charges.
Conclusion
Amazon DynamoDB is a managed, serverless NoSQL database designed for scalable key-value and document workloads. It offers fast key-based access, automatic infrastructure management, secondary indexes, conditional updates, ACID transactions, Streams, TTL, and multi-Region replication options.
The most important architectural lesson is that DynamoDB data modeling starts with access patterns. Efficient queries require suitable partition keys, sort keys, and indexes. Poor key distribution, unnecessary scans, missing concurrency controls, or incorrect consistency assumptions can undermine the performance and reliability that DynamoDB is designed to provide.
Key Takeaway: Choose DynamoDB when your application benefits from predictable key-based access and managed horizontal scale. Design keys around real queries, protect concurrent writes with conditions or transactions, and treat partition distribution, pagination, and idempotency as first-class production concerns.
More Articles to Read
These additional articles explore the distributed systems concepts behind DynamoDB's scalability, consistency, indexing, and concurrency behavior.
- What Is Consistent Hashing?
- Database Scaling Explained: Vertical vs Horizontal Scaling
- What Is a Database Transaction?
- What Is a Distributed Lock?
- What Is an Idempotency Key?
- Leader Election and Distributed Coordination
- Consistency in System Design
- Strong Consistency in Databases
- What Is a Lamport Clock?
Comments (0)