Learn what Amazon DynamoDB is, how its NoSQL model works, and practical design patterns for scalable, low-latency systems and microservices.

Amazon DynamoDB is a fully managed NoSQL database service from AWS, designed for applications that need consistently low-latency reads and writes at virtually any scale. “Fully managed” means AWS handles infrastructure work—hardware provisioning, replication, patching, and many operational tasks—so teams can focus on shipping features instead of running database servers.
At its core, DynamoDB stores data as items (rows) inside tables, but each item can have flexible attributes. The data model is best understood as a mix of:
Teams choose DynamoDB when they want predictable performance and simpler operations for workloads that don’t fit neatly into relational joins. It’s commonly used for microservices (each service owning its data), serverless apps with bursty traffic, and event-driven systems that react to changes in data.
This post walks through the building blocks (tables, keys, and indexes), how to model around access patterns (including single-table design), how scaling and capacity modes work, and practical patterns for streaming changes into an event-driven architecture.
DynamoDB is organized around a few simple building blocks, but the details matter because they determine how you model data and how fast (and cost-effective) requests will be.
A table is the top-level container. Each record in a table is an item (similar to a row), and each item is a set of attributes (similar to columns).
Unlike relational databases, items in the same table don’t need to share the same attributes. One item might have {status, total, customerId}, while another includes {status, shipmentTracking}—DynamoDB doesn’t require a fixed schema.
Every item is uniquely identified by a primary key, and DynamoDB supports two types:
In practice, composite keys enable “grouped” access patterns like “all orders for a customer, newest first.”
A Query reads items by primary key (or an index key). It targets a specific partition key and can filter by sort key ranges—this is the efficient, preferred path.
A Scan walks the whole table (or index) and then filters. It’s easy to start with, but it’s usually slower and more expensive at scale.
A few constraints you’ll feel early:
These fundamentals set up everything that follows: access patterns, indexing choices, and performance characteristics.
DynamoDB is often described as both a key-value store and a document database. That’s accurate, but it helps to understand what each implies in day-to-day design.
At its core, you retrieve data by key. Provide the primary key values and DynamoDB returns a single item. That keyed lookup is what delivers predictable, low-latency storage for many workloads.
At the same time, an item can contain nested attributes (maps and lists), which makes it feel like a document database: you can store structured payloads without defining a rigid schema upfront.
Items map naturally to JSON-like data:
profile.name, profile.address).This is a great fit when an entity is usually read as a whole—like a user profile, a shopping cart, or a configuration bundle.
DynamoDB doesn’t support server-side joins. If your app needs to fetch “an order plus its line items plus shipping status” in one read path, you’ll often denormalize: copy some attributes into multiple items, or embed small substructures directly inside an item.
Denormalization increases write complexity and can create update fan-out. The payoff is fewer round trips and faster reads—often the critical path in scalable systems.
The fastest DynamoDB queries are the ones you can express as “give me this partition” (and optionally “within this partition, give me this range”). That’s why key choice is primarily about how you read data, not just how you store it.
The partition key determines which physical partition stores an item. DynamoDB hashes this value to spread data and traffic. If many requests concentrate on a small set of partition key values, you can create “hot” partitions and hit throughput limits even if the table is mostly idle.
Good partition keys:
"GLOBAL")With a sort key, items sharing the same partition key are stored together and ordered by the sort key. This enables efficient:
BETWEEN, begins_with)A common pattern is composing the sort key, such as TYPE#id or TS#2025-12-22T10:00:00Z, to support multiple query shapes without extra tables.
PK = USER#<id> (simple )If your partition key aligns with your highest-volume queries and distributes evenly, you get consistently low-latency reads and writes. If it doesn’t, you’ll compensate with scans, filters, or extra indexes—each adding cost and increasing the risk of hot keys.
Secondary indexes give DynamoDB alternate query paths beyond your table’s primary key. Instead of reshaping your base table every time a new access pattern appears, you can add an index that re-keys the same items for a different query.
A Global Secondary Index (GSI) has its own partition key (and optional sort key) that can be completely different from the table’s. It’s “global” because it spans all table partitions and can be added or removed at any time. Use a GSI when you need a new access pattern that doesn’t fit the original key design—for example, querying orders by customerId when the table is keyed by orderId.
A Local Secondary Index (LSI) shares the same partition key as the base table but uses a different sort key. LSIs must be defined at table creation. They’re useful when you want multiple sort orders within the same entity group (same partition key), like fetching a customer’s orders sorted by createdAt vs. status.
Projection determines which attributes DynamoDB stores in the index:
Every write to the base table can trigger writes to one or more indexes. More GSIs plus wider projections increases write costs and capacity consumption. Plan indexes around stable access patterns, and keep projected attributes minimal when possible.
DynamoDB scaling starts with a choice: On-Demand or Provisioned capacity. Both can reach very high throughput, but they behave differently under changing traffic.
On-Demand is the simplest: you pay per request and DynamoDB automatically accommodates variable load. It’s a good fit for unpredictable traffic, early-stage products, and spiky workloads where you don’t want to manage capacity targets.
Provisioned is capacity planning: you specify (or auto-scale) read and write throughput and get more predictable pricing at steady usage. It’s often cheaper for known, consistent workloads and for teams that can forecast demand.
Provisioned throughput is measured in:
Your item size and access pattern determine real cost: larger items, strong consistency, and scans can burn capacity quickly.
Auto scaling adjusts provisioned RCUs/WCUs based on utilization targets. It helps with gradual growth and predictable cycles, but it’s not instant. Sudden spikes can still throttle if capacity doesn’t ramp fast enough, and it can’t fix a hot partition key that concentrates traffic on one partition.
DynamoDB Accelerator (DAX) is an in-memory cache that can reduce read latency and offload repeated reads (e.g., popular product pages, session lookups, leaderboards). It’s most useful when many clients repeatedly request the same items; it won’t help write-heavy patterns, and it doesn’t replace careful key design.
DynamoDB lets you trade off read guarantees against latency and cost, so it’s important to be explicit about what “correct” means for each operation.
By default, GetItem and Query use eventually consistent reads: you might briefly see an older value right after a write. This is often fine for feeds, product catalogs, and other read-mostly views.
With strongly consistent reads (an option for reads from the base table in a single region), DynamoDB guarantees you see the latest acknowledged write. Strong consistency costs more read capacity and can increase tail latency, so reserve it for truly critical reads.
Strong consistency is valuable for reads that gate irreversible actions:
For counters, the safest approach is typically not “strong read then write,” but an atomic update (e.g., UpdateItem with ADD) so increments aren’t lost.
DynamoDB transactions (TransactWriteItems, TransactGetItems) provide ACID semantics across up to 25 items. They’re useful when you must update multiple items together—like writing an order and reserving inventory—or enforcing invariants that can’t tolerate intermediate states.
Retries are normal in distributed systems. Make writes idempotent so retries don’t duplicate effects:
ConditionExpression (e.g., “only create if attribute_not_exists”)Correctness in DynamoDB is mostly about choosing the right consistency level and designing operations so retries can’t break your data.
DynamoDB stores table data across multiple physical partitions. Each partition has finite throughput for reads and writes, plus a limit on how much data it can hold. Your partition key determines where an item lives; if too many requests target the same partition key value (or a small set of values), that partition becomes the bottleneck.
Hot partitions are usually caused by key choices that concentrate traffic: a “global” partition key like USER#1, TENANT#default, or STATUS#OPEN, or time-ordered patterns where everyone writes to “now” under one partition key.
You’ll typically see:
ProvisionedThroughputExceededException) for a subset of keysDesign for distribution first, then query convenience:
TENANT#<id> instead of a shared constant).ORDER#<id>#<shard> to spread writes across N shards, then query across shards when needed.METRIC#2025-12-22T10) to prevent “all writes go to the latest item.”For unpredictable spikes, on-demand capacity can absorb bursts (within service limits). With provisioned capacity, use auto scaling and implement client-side exponential backoff with jitter on throttles to avoid synchronized retries that amplify the spike.
DynamoDB data modeling starts from access patterns, not from ER diagrams. You design keys so the queries you need become fast Query operations, while everything else is either avoided or handled asynchronously.
“Single-table design” means storing multiple entity types (users, orders, messages) in one table and using consistent key conventions to fetch related data in a single Query. This reduces cross-entity round trips and keeps latency predictable.
A common approach is composite keys:
PK groups a logical partition (e.g., USER#123)SK orders items within that group (e.g., PROFILE, ORDER#2025-12-01, MSG#000123)This lets you fetch “everything for a user” or “only orders for a user” by choosing a sort-key prefix.
For graph-like relationships, an adjacency list works well: store edges as items.
PK = USER#123, SK = FOLLOWS#USER#456To support reverse lookups or true many-to-many, add an inverted edge item or project to a GSI, depending on read paths.
For events and metrics, avoid unbounded partitions by bucketing:
PK = DEVICE#9#2025-12-22 (device + day)SK = TS#1734825600 (timestamp)Use TTL to expire old points automatically, and keep aggregates (hourly/daily rollups) as separate items for fast dashboards.
If you want a deeper refresher on key conventions, see /blog/partition-key-and-sort-key-design.
DynamoDB Streams is DynamoDB’s built-in change data capture (CDC) feed. When enabled on a table, every insert, update, or delete produces a stream record that downstream consumers can react to—without polling the table.
A stream record contains keys and (optionally) the item’s old and/or new image, depending on the stream view type you choose (keys only, new image, old image, both). Records are grouped into shards, which you read sequentially.
A common setup is DynamoDB Streams → AWS Lambda, where each batch of records triggers a function. Other consumers are possible too (custom consumers, or piping into analytics/logging systems).
Typical workflows include:
This keeps the primary table optimized for low-latency reads/writes while pushing derived work to asynchronous consumers.
Streams provide ordered processing per shard (which typically correlates with the partition key), but there is no global ordering across all keys. Delivery is at-least-once, so duplicates can happen.
To handle this safely:
Designed with these guarantees in mind, Streams can turn DynamoDB into a solid backbone for event-driven systems.
DynamoDB is designed for high availability by spreading data across multiple Availability Zones within a region. For most teams, the practical reliability wins come from having a clear backup strategy, understanding replication options, and monitoring the right metrics.
On-demand backups are manual (or automated) snapshots you take when you want a known restore point—before a migration, after a release, or prior to a big backfill. They’re great for “bookmark” moments.
Point-in-time recovery (PITR) continuously captures changes so you can restore the table to any second within the retention window. PITR is the safety net for accidental deletes, bad deploys, or malformed writes that slip past validation.
If you need multi-region resilience or low-latency reads near users, Global Tables replicate data across selected regions. They simplify failover planning, but introduce cross-region replication delay and conflict-resolution considerations—so keep write patterns and item ownership clear.
At minimum, alert on:
These signals usually surface hot-partition issues, insufficient capacity, or unexpected access patterns.
For throttling, first identify the access pattern causing it, then mitigate by temporarily switching to on-demand or increasing provisioned capacity, and consider sharding hot keys.
For partial outages or elevated errors, reduce blast radius: disable non-critical traffic, retry with jittered backoff, and fail gracefully (for example, serving cached reads) until the table stabilizes.
DynamoDB security is mostly about tightening who can call which API actions, from where, and on what keys. Because tables can hold many entity types (and sometimes many tenants), access control should be designed alongside the data model.
Start with identity-based IAM policies that limit actions (for example, dynamodb:GetItem, Query, PutItem) to the minimum set and scope them to specific table ARNs.
For finer control, use dynamodb:LeadingKeys to restrict access by partition key values—useful when a service or tenant should only read/write items in its own keyspace.
DynamoDB encrypts data at rest by default using AWS owned keys or a customer-managed KMS key. If you have compliance requirements, verify:
For encryption in transit, ensure clients use HTTPS (AWS SDKs do by default). If you terminate TLS in a proxy, confirm the hop between the proxy and DynamoDB is still encrypted.
Use a VPC Gateway Endpoint for DynamoDB so traffic stays on the AWS network and you can apply endpoint policies to constrain access. Pair this with egress controls (NACLs, security groups, and routing) to avoid “anything can reach the public internet” paths.
For shared tables, include a tenant identifier in the partition key (for example, TENANT#<id>), then enforce tenant isolation with IAM conditions on dynamodb:LeadingKeys.
If you need stronger isolation, consider separate tables per tenant or per environment, and reserve shared-table designs for cases where operational simplicity and cost efficiency outweigh stricter blast-radius requirements.
DynamoDB is often “cheap when you’re precise, expensive when you’re vague.” Costs typically follow your access patterns, so the best optimization work starts by making those patterns explicit.
Your bill is mainly shaped by:
A common surprise: every write to a table is also a write to each affected GSI, so “just one more index” can multiply write cost.
Good key design reduces the need for expensive operations. If you frequently reach for Scan, you’re paying to read data you’ll throw away.
Prefer:
Query by partition key (and optionally sort key conditions)If an access pattern is rare, consider serving it via a separate table, an ETL job, or a cached read model rather than a permanent GSI.
Use TTL to automatically delete short-lived items (sessions, temporary tokens, intermediate workflow state). This trims storage and can keep indexes smaller over time.
For append-heavy data (events, logs), combine TTL with sort-key designs that let you query “recent only,” so you don’t routinely touch cold history.
In provisioned mode, set conservative baselines and scale with auto scaling based on real metrics. In on-demand mode, watch for inefficient patterns (large items, chatty clients) that drive request volume.
Treat Scan as a last resort—when you truly need full-table processing, schedule it off-peak or run it as a controlled batch with pagination and backoff.
DynamoDB shines when your application can be expressed as a set of well-defined access patterns and you need consistently low latency at high scale. If you can describe your reads and writes up front (by partition key, sort key, and a small number of indexes), it’s often one of the simplest ways to operate a highly available data store.
DynamoDB is a strong choice when you have:
Look elsewhere if your core requirements include:
Many teams keep DynamoDB for “hot” operational reads and writes, then add:
If you’re validating access patterns and single-table conventions, speed matters. Teams sometimes prototype the surrounding service and UI in Koder.ai (a vibe-coding platform that builds web, server, and mobile apps from chat) and then iterate on the DynamoDB key design as real query paths emerge. Even if your production backend differs, quick end-to-end prototypes help reveal which queries should be Query operations versus which ones would accidentally become expensive scans.
Validate: (1) your top queries are known and key-based, (2) correctness needs match the consistency model, (3) expected item sizes and growth are understood, and (4) the cost model (on-demand vs. provisioned plus autoscaling) fits your budget.
DynamoDB is a fully managed NoSQL database on AWS designed for consistently low-latency reads/writes at very high scale. Teams use it when they can define key-based access patterns (fetch by ID, list by owner, time-range queries) and want to avoid running database infrastructure.
It’s especially common in microservices, serverless apps, and event-driven systems.
A table holds items (like rows). Each item is a flexible set of attributes (like columns) and can include nested data.
DynamoDB works well when one request typically needs “the whole entity,” because items can contain maps and lists (JSON-like structures).
A partition key alone uniquely identifies an item (simple primary key). A partition key + sort key (composite key) lets multiple items share the same partition key while staying uniquely identifiable and ordered by the sort key.
Composite keys enable patterns like:
Use Query when you can specify the partition key (and optionally a sort key condition). It’s the fast, scalable path.
Use Scan only when you truly need to read everything; it reads the whole table or index and filters after the fact, which is usually slower and more expensive.
If you’re scanning frequently, it’s a sign your key or index design needs adjustment.
Secondary indexes provide alternate query paths.
Indexes increase write cost because writes are replicated into the index.
Choose On-Demand if traffic is unpredictable, spiky, or you don’t want to manage capacity. You pay per request.
Choose Provisioned if usage is steady/predictable and you want more controlled costs. Pair it with auto scaling, but remember it may not react instantly to sudden spikes.
By default, reads are eventually consistent, meaning you might briefly read older data right after a write.
Use strongly consistent reads (when available) for critical “must be current” checks, such as authorization gates or workflow state transitions.
For correctness under concurrency, prefer atomic updates (e.g., conditional writes or ADD) over read-modify-write loops.
Transactions (TransactWriteItems, TransactGetItems) provide ACID guarantees across up to 25 items.
Use them when you must update multiple items together (e.g., create an order and reserve inventory) or enforce invariants that can’t tolerate partial updates.
They cost more and add latency, so reserve them for the flows that truly require them.
Hot partitions happen when too many requests target the same partition key value (or a small set of values), causing throttling even if the table is otherwise underused.
Common mitigations:
Enable DynamoDB Streams to get a change feed for inserts, updates, and deletes. A common pattern is Streams → Lambda to trigger downstream work.
Important guarantees to design for:
Make consumers (upsert by key, use conditional writes, or track processed event IDs).
GetItemPK = USER#<id>, SK begins_with ORDER# (or SK = CREATED_AT#...)PK = DEVICE#<id>, SK = TS#<timestamp> with BETWEEN for time windows