
MongoDB vs. DynamoDB: A Practical Comparison
If you're building on AWS and need a NoSQL database, you'll quickly narrow the choice to two options: Amazon DynamoDB, the fully managed key-value and document store built into AWS, and MongoDB, usually through MongoDB Atlas running on AWS. Both store JSON-like data, both scale to enormous workloads, and both are fully managed if you want them to be. On the surface, they look interchangeable.
They aren't. DynamoDB is built around a single idea: every access is a fast lookup by key, and you design your table so that every query your application needs is one of those lookups. MongoDB is a general-purpose document database with secondary indexes, a rich query language, and an aggregation framework, which means you can ask questions you didn't plan for. That difference ripples into data modeling, cost, operational behavior, and how painful it is to change your mind later.
This guide compares the two on data model, querying, indexing, transactions, scaling, pricing structure, operations, and ecosystem, using the same example in both, and ends with guidance on which fits which kind of project.
The Data Model
DynamoDB: tables, items, and keys
A DynamoDB table holds items, each identified by a primary key:
- A partition key alone (simple primary key), or
- A partition key plus a sort key (composite primary key).
The partition key determines which physical partition stores the item. The sort key orders items within a partition, which enables range queries like "all orders for customer X in September." Every other attribute is schemaless, and item size is capped at 400 KB.
MongoDB: collections and documents
A MongoDB collection holds documents with a unique _id. Documents can be nested arbitrarily, contain arrays, and be up to 16 MB. You can create secondary indexes on any field, including nested fields and array elements, and query on any combination of fields.
The same example in both
Say you're building an order system. You need to:
- Get a customer's profile.
- List a customer's orders, newest first.
- Get an order with its line items.
- Find all orders with status
pendingplaced more than an hour ago.
In DynamoDB, a common approach is single-table design, where different entity types share one table with generic key names:
| PK | SK | Attributes |
|---|---|---|
CUSTOMER#42 | PROFILE | name, email, tier |
CUSTOMER#42 | ORDER#2026-09-18#9001 | status, total |
CUSTOMER#42 | ORDER#2026-09-19#9002 | status, total |
ORDER#9001 | ITEM#1 | sku, qty, price |
ORDER#9001 | ITEM#2 | sku, qty, price |
Access patterns 1 through 3 are now efficient GetItem or Query calls on the key. Pattern 4 needs a global secondary index (GSI) with, for example, status as its partition key and createdAt as its sort key.
In MongoDB, you'd likely model it as two collections:
// customers
{
_id: 42,
name: "Ana Ruiz",
email: "ana@example.com",
tier: "gold"
}
// orders
{
_id: 9001,
customerId: 42,
status: "pending",
total: Decimal128("129.90"),
createdAt: ISODate("2026-09-18T10:21:00Z"),
items: [
{ sku: "LAMP-01", qty: 1, price: Decimal128("89.90") },
{ sku: "BULB-04", qty: 2, price: Decimal128("20.00") }
]
}
With two indexes:
db.orders.createIndex({ customerId: 1, createdAt: -1 });
db.orders.createIndex({ status: 1, createdAt: 1 });
Every access pattern is covered, and the line items are embedded, so fetching an order returns everything in one read.
Querying
This is the biggest practical difference.
In DynamoDB, efficient reads are GetItem (one item by full key) and Query (items within one partition, optionally filtered by a sort key condition). Here's access pattern 2 with the AWS SDK for JavaScript v3:
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { DynamoDBDocumentClient, QueryCommand } from "@aws-sdk/lib-dynamodb";
const ddb = DynamoDBDocumentClient.from(new DynamoDBClient({}));
const { Items } = await ddb.send(
new QueryCommand({
TableName: "shop",
KeyConditionExpression: "PK = :pk AND begins_with(SK, :prefix)",
ExpressionAttributeValues: {
":pk": "CUSTOMER#42",
":prefix": "ORDER#",
},
ScanIndexForward: false,
Limit: 20,
}),
);
There's also a FilterExpression, but it's applied after items are read, so you still pay for reading everything the key condition matched. Anything that isn't expressible as a key lookup either needs a new GSI or a Scan, which reads the whole table.
The same query in MongoDB with the Node.js driver:
const orders = await db
.collection("orders")
.find({ customerId: 42 })
.sort({ createdAt: -1 })
.limit(20)
.toArray();
And when a new requirement arrives, like "total revenue by tier for gold customers last month," MongoDB can answer it with an aggregation:
db.orders.aggregate([
{
$match: {
createdAt: {
$gte: ISODate("2026-08-01T00:00:00Z"),
$lt: ISODate("2026-09-01T00:00:00Z"),
},
},
},
{
$lookup: {
from: "customers",
localField: "customerId",
foreignField: "_id",
as: "customer",
},
},
{ $unwind: "$customer" },
{ $match: { "customer.tier": "gold" } },
{ $group: { _id: null, revenue: { $sum: "$total" }, orders: { $sum: 1 } } },
]);
In DynamoDB, that question usually means exporting data to S3 and running it through Athena, or streaming changes into an analytics store. DynamoDB has no joins, no aggregation framework, and no ad hoc query planner. PartiQL gives you SQL-like syntax, but it runs on the same key-based access model underneath, so a PartiQL SELECT that doesn't hit a key becomes a scan.
Indexing
| Feature | DynamoDB | MongoDB |
|---|---|---|
| Primary index | Partition key (+ optional sort key) | _id (unique) |
| Secondary indexes | GSIs (limited number per table), LSIs (at creation only) | Many per collection, on any fields, created anytime |
| Compound indexes | Partition + sort key only | Any number of fields |
| Array / multikey indexes | No | Yes |
| Partial / sparse indexes | Sparse by nature (items without the key are omitted) | Partial and sparse indexes |
| Text / search | No (use OpenSearch) | Text indexes; Atlas Search |
| Geospatial | No native support | 2dsphere indexes |
| Index consistency | GSIs are eventually consistent | Indexes are updated with the write |
GSIs in DynamoDB are separate, eventually consistent copies of your data with their own capacity and cost. They're powerful, but each one is a design decision with a price attached. In MongoDB, adding an index is a routine operation.
Consistency and Transactions
DynamoDB reads are eventually consistent by default. You can request strongly consistent reads on the base table (at double the read cost), but GSI reads are always eventually consistent. DynamoDB supports transactions through TransactWriteItems and TransactGetItems, with a limit on the number of items per transaction (100 in recent versions) and a higher cost per operation.
MongoDB reads from the primary are strongly consistent by default, and you can tune behavior with read concern, write concern, and read preference. Single-document operations are atomic, and multi-document ACID transactions work across collections and shards. Because related data is often embedded, many operations don't need a transaction in the first place: updating an order and its items is one atomic updateOne. For the knobs involved, see MongoDB read preferences and write concerns explained.
Scaling and Performance
DynamoDB scales horizontally and automatically. Data is split across partitions by partition key, and AWS adds partitions as data and traffic grow. Performance is famously consistent: single-digit millisecond latency for key lookups at almost any scale. You never think about servers, and there is no instance size.
The catch is partition key design. Each partition has throughput limits, and a hot key (a single celebrity user, a global counter, today's date as a partition key) can be throttled even when the table overall has spare capacity. Adaptive capacity helps, but good key distribution is still your responsibility.
MongoDB scales vertically within a replica set and horizontally through sharding. Sharding also depends on a good key choice (how to choose a good shard key covers the rules), but many applications never need it because a single replica set handles substantial workloads. Performance depends on index design, working set size, and cluster tier: well-indexed queries are fast, poorly indexed ones aren't, and you have more ways to get it wrong and more ways to fix it.
A fair summary: DynamoDB gives you predictable performance for predictable access patterns, with almost no operational tuning. MongoDB gives you flexible performance across a much wider range of queries, with more tuning available and required.
Pricing Structure
Both pricing models change over time, so check the current AWS and Atlas pricing pages for numbers. What matters is how each one charges.
DynamoDB charges for:
- Reads and writes, either per request (on-demand mode) or as provisioned capacity per second (provisioned mode, optionally with auto-scaling and reserved capacity).
- Storage per GB-month.
- GSIs, which consume their own write capacity for every base-table write that affects them, plus storage.
- Optional features: backups, point-in-time recovery, global tables replication, streams reads, exports.
Item size matters: reads are billed in 4 KB units and writes in 1 KB units, so large items and large index projections cost more per operation.
MongoDB Atlas charges for:
- Cluster tier (compute and memory) per hour, for each node.
- Storage, backups, and data transfer.
- Add-ons like search nodes or analytics nodes.
The practical consequence: DynamoDB cost scales with traffic, which is fantastic for spiky or low-volume workloads (on-demand mode costs almost nothing when idle) and can get expensive for sustained high-throughput workloads or heavy use of GSIs. Atlas cost scales with provisioned capacity, so a cluster costs the same whether it's busy or idle, and heavy sustained throughput doesn't add per-request charges.
Operations
DynamoDB is about as hands-off as databases get. No servers, no versions to upgrade, no replica sets. Global tables provide multi-region, multi-active replication. DynamoDB Streams provide a change log you can process with Lambda.
MongoDB Atlas is also fully managed, but you choose a cluster tier, a region layout, and backup settings, and you're responsible for indexes and query performance. In return, you can run it on AWS, Google Cloud, or Azure, move between clouds, or self-host the same database on your own infrastructure. Change streams provide the equivalent of DynamoDB Streams, and Atlas Triggers can process them without extra services.
Local Development and Portability
DynamoDB has DynamoDB Local for development, which emulates the API but not every behavior. Your data model and code are tied to AWS; moving off DynamoDB means rewriting data access.
MongoDB runs the same server everywhere: on your laptop in Docker, in CI, in Atlas, or on your own servers. That makes local development and testing faithful to production, and avoids lock-in at the database level.
Side-by-Side Summary
| Dimension | DynamoDB | MongoDB |
|---|---|---|
| Model | Key-value / wide item | Document |
| Max item/document size | 400 KB | 16 MB |
| Query flexibility | Key-based; plan every access pattern upfront | Ad hoc queries, aggregations, joins |
| Secondary indexes | Limited GSIs/LSIs, eventually consistent GSIs | Many, strongly consistent, rich types |
| Transactions | Yes, with item limits | Multi-document ACID, cross-shard |
| Scaling | Automatic, serverless | Vertical + sharding |
| Pricing driver | Requests and storage | Provisioned cluster capacity |
| Cloud | AWS only | AWS, Google Cloud, Azure, self-hosted |
When DynamoDB Is the Better Fit
- Access patterns are known, stable, and key-based: session stores, shopping carts, user settings, idempotency keys, device state.
- Traffic is spiky or unpredictable, and scale-to-near-zero cost matters.
- You're all-in on AWS serverless: Lambda, API Gateway, Step Functions, EventBridge. DynamoDB integrates natively with IAM and these services.
- You want zero database operations, and your team is comfortable with single-table design.
When MongoDB Is the Better Fit
- Access patterns are evolving or you need to support ad hoc queries, filtering on many fields, or reporting.
- Your data is rich and nested, with documents that exceed DynamoDB's item size or need array queries.
- You need aggregations, joins, text search, or geospatial queries without bolting on other services.
- Multi-cloud or portability matters, including running the same database locally and on-premises.
- Sustained high throughput where per-request pricing would add up.
Common Mistakes
Designing a DynamoDB table like a relational schema. One table per entity with IDs pointing at each other leads to many round trips and no joins. DynamoDB works when you design for access patterns first.
Relying on Scan and FilterExpression. Filters don't reduce read cost. If you find yourself scanning, you need a GSI or a different database.
Porting single-table design to MongoDB. Generic PK/SK fields and overloaded entity types throw away MongoDB's strengths. Use separate collections, real field names, and secondary indexes.
Ignoring hot keys in either system. A partition key in DynamoDB or a shard key in MongoDB that concentrates traffic will throttle or overload one partition or shard. Spread writes.
Choosing based only on the free tier. Both have generous entry points. Model your expected steady-state traffic, storage, and indexes in both pricing models before deciding.
Conclusion
DynamoDB and MongoDB are both excellent, but they're built on different assumptions. DynamoDB assumes you know your access patterns upfront and rewards you with effortless scaling, serverless operations, and pay-per-request pricing. MongoDB assumes your questions will change and gives you secondary indexes, a full query language, aggregations, and transactions to answer them, with portability across clouds and your own servers.
Before choosing, write down every query your application needs today and three you can imagine needing next year. If all of them are clean key lookups, DynamoDB is a strong choice. If several of them would need a new index, a filter across many fields, or an aggregation, MongoDB will make your life much easier.


