Loading content...
DynamoDB is a remarkable database. It powers Amazon's retail operations, handles trillions of requests daily, and provides single-digit millisecond latency at virtually any scale. It's also a database that can cause immense frustration when used for the wrong workload.
I've seen teams achieve phenomenal success with DynamoDB—zero-downtime deployments, linear cost scaling, and operational simplicity that freed engineers to focus on product. I've also seen teams struggle for months, fighting against DynamoDB's constraints, eventually migrating to a different database at significant cost.
The difference wasn't the teams' skill level. It was fit. DynamoDB is exceptional at what it's designed for and deliberately limited in other areas. This page helps you determine whether DynamoDB is the right choice for your system—before you commit to it.
By the end of this page, you will understand the characteristics of systems that thrive on DynamoDB, warning signs that suggest a different database, how DynamoDB compares to alternatives like Aurora, MongoDB, and Cassandra, strategies for hybrid architectures, and migration considerations for both directions.
DynamoDB is purpose-built for specific workload characteristics. When your system aligns with these, DynamoDB delivers unmatched value.
| Application Type | Why DynamoDB Fits | Key Features Used |
|---|---|---|
| E-commerce (cart, orders) | Simple key access, high scale, session-like data | TTL, GSIs, transactions |
| Gaming (leaderboards, sessions) | Sub-ms latency, atomic counters, player state | Atomic counters, sparse indexes |
| IoT telemetry | Massive write throughput, time-series patterns | TTL for retention, write sharding |
| Ad tech (bidding, profiles) | Extreme latency requirements, high throughput | DAX caching, on-demand scaling |
| Social (feeds, notifications) | Known access patterns, timeline queries | Composite keys, sort key ranges |
| Content metadata | Flexible schema, varied attributes | Schemaless items, sparse indexes |
| Serverless backends | No connection limits, HTTP API | Lambda integration, on-demand |
Before choosing DynamoDB, write down every query your application will need—not might need, but will definitely need. If you can design a schema that serves all these queries efficiently (maybe with GSIs), DynamoDB is likely a good fit. If you're thinking 'we'll figure it out later' or 'we need to join these tables,' reconsider.
Just as important as knowing when DynamoDB shines is recognizing when it's the wrong tool. These warning signs suggest you should consider alternatives:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465
// ============================================// ❌ WARNING SIGN: Complex joining required// ============================================// If your queries look like this, DynamoDB is NOT the answer: ```sqlSELECT o.order_id, c.name AS customer_name, p.name AS product_name, s.carrier AS shipping_carrierFROM orders oJOIN customers c ON o.customer_id = c.idJOIN products p ON o.product_id = p.idJOIN shipping s ON o.shipping_id = s.idWHERE o.created_at > '2024-01-01' AND c.region = 'US' AND p.category = 'Electronics'ORDER BY o.total DESCLIMIT 100;``` // DynamoDB cannot do this efficiently.// You'd need: multiple queries, application-side joins, denormalization.// If this is your primary workload, use PostgreSQL or MySQL. // ============================================// ❌ WARNING SIGN: Aggregations are primary workload// ============================================// Queries like this are painful in DynamoDB: ```sqlSELECT DATE(created_at) as date, product_category, COUNT(*) as order_count, SUM(total) as revenue, AVG(total) as avg_order_valueFROM ordersWHERE created_at BETWEEN '2024-01-01' AND '2024-06-30'GROUP BY DATE(created_at), product_categoryORDER BY date, revenue DESC;``` // DynamoDB would require:// 1. Scan entire table (expensive at scale)// 2. Aggregate in application code// 3. Or: Maintain pre-aggregated metrics tables via Streams // For analytics, use: Redshift, BigQuery, or at minimum Aurora. // ============================================// ⚠️ WARNING SIGN: Unknown future access patterns// ============================================// Early-stage startup conversation: // Product: "We need to store user data."// Engineer: "What queries will we need?"// Product: "Not sure yet. We'll figure it out as we go." // This is a warning sign for DynamoDB!// Better choice: Start with PostgreSQL. Migrate to DynamoDB later// if/when access patterns stabilize and scale demands it.Early in a product's life, requirements change rapidly. DynamoDB's rigid schema design (you can't add GSIs with different partition keys retroactively as easily as adding SQL indexes) can become a constraint. Consider starting with a relational database and migrating to DynamoDB once access patterns stabilize—if scale justifies the move.
Let's compare DynamoDB to common alternatives across dimensions that matter for real-world system design:
| Dimension | DynamoDB | Aurora / RDS PostgreSQL |
|---|---|---|
| Query model | Key-based, limited filtering | Full SQL, complex joins, aggregations |
| Schema flexibility | Schemaless items | Fixed schema with migrations |
| Scaling | Automatic, unlimited horizontal | Vertical limited; read replicas for reads |
| Transactions | 25 items max, simple operations | Full ACID, complex transactions |
| Operations | Fully managed, zero maintenance | Managed but requires sizing, maintenance |
| Latency | Single-digit ms at any scale | Low ms but degrades at extreme scale |
| Cost at low scale | Can be expensive (on-demand) | Often cheaper for small workloads |
| Cost at high scale | Often cheaper (no over-provisioning) | Can become expensive with big instances |
| Best for | Known patterns, massive scale | Complex queries, evolving requirements |
| Dimension | DynamoDB | MongoDB (Atlas) |
|---|---|---|
| Data model | Key-value / document hybrid | Rich document model |
| Query language | Limited PartiQL, SDK operations | Full MongoDB Query Language (MQL) |
| Indexing | GSI/LSI, 20 limit, partition key required | Flexible indexes, compound, partial, text |
| Aggregations | Minimal (precompute required) | Aggregation pipeline (powerful) |
| Managed option | Fully managed (AWS) | Atlas (fully managed, multi-cloud) |
| Multi-region | Global Tables (active-active) | Atlas Global Clusters (similar) |
| Scale | Virtually unlimited | High but manual sharding required |
| Best for | AWS-native, extreme scale | Document flexibility, richer queries |
| Dimension | DynamoDB | Apache Cassandra |
|---|---|---|
| Deployment | Managed only | Self-managed or DataStax Astra (managed) |
| Operations | Zero ops | Significant operational expertise needed |
| Consistency model | Eventual + strong (on base table) | Tunable consistency (quorum-based) |
| Query model | Key-value with indexes | CQL (SQL-like), partition-aware |
| Write throughput | Excellent (managed sharding) | Excellent (write-optimized) |
| Cost | Pay as you go | Infrastructure cost (can be lower at scale) |
| Vendor lock-in | AWS only | Open source, multi-cloud |
| Best for | Managed with AWS ecosystem | Self-managed, multi-cloud, cost control |
DynamoDB's pricing (especially on-demand) can seem expensive per-operation. But the total cost of ownership includes engineering time saved on operations, avoided on-call incidents, and eliminated capacity planning. For teams without DBA expertise, these hidden costs often exceed DynamoDB's premium.
Use this framework to systematically evaluate whether DynamoDB is right for your system:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192
┌─────────────────────────────────────────────────────────────────────┐│ DynamoDB Decision Framework │└─────────────────────────────────────────────────────────────────────┘ START │ ▼┌─────────────────────────────────────────────────────────────────────┐│ Q1: Are your access patterns well-defined and stable? ││ (You know what queries you need before building) │└─────────────────────────────────────────────────────────────────────┘ │ │ YES NO │ │ ▼ ▼ Continue ┌────────────────────────┐ │ │ Consider: PostgreSQL │ │ │ Reason: Schema/query │ │ │ flexibility for │ │ │ evolving requirements │ │ └────────────────────────┘ ▼┌─────────────────────────────────────────────────────────────────────┐│ Q2: Do most queries access data by known keys (not arbitrary search)?│└─────────────────────────────────────────────────────────────────────┘ │ │ YES NO │ │ ▼ ▼ Continue ┌────────────────────────┐ │ │ Consider: Elasticsearch│ │ │ or PostgreSQL with │ │ │ full-text search │ │ └────────────────────────┘ ▼┌─────────────────────────────────────────────────────────────────────┐│ Q3: Is your data relational with complex joins across entities? │└─────────────────────────────────────────────────────────────────────┘ │ │ NO YES │ │ ▼ ▼ Continue ┌────────────────────────┐ │ │ Consider: Aurora/RDS │ │ │ Reason: Joins are │ │ │ native to SQL │ │ └────────────────────────┘ ▼┌─────────────────────────────────────────────────────────────────────┐│ Q4: Do you need heavy aggregations (GROUP BY, analytics)? │└─────────────────────────────────────────────────────────────────────┘ │ │ NO YES │ │ ▼ ▼ Continue ┌────────────────────────┐ │ │ Consider: Data │ │ │ warehouse (Redshift, │ │ │ BigQuery) + DynamoDB │ │ │ for OLTP │ │ └────────────────────────┘ ▼┌─────────────────────────────────────────────────────────────────────┐│ Q5: Do you need scale beyond what single-node databases provide? ││ (Millions of requests/second, petabytes of data) │└─────────────────────────────────────────────────────────────────────┘ │ │ YES NO │ │ ▼ ▼ Continue ┌────────────────────────┐ │ │ DynamoDB may be │ │ │ overkill. PostgreSQL │ │ │ is simpler for small │ │ │ workloads. But │ │ │ DynamoDB still valid │ │ │ if ops simplicity │ │ │ matters. │ │ └────────────────────────┘ ▼┌─────────────────────────────────────────────────────────────────────┐│ Q6: Is your team on AWS and willing to use AWS-specific services? │└─────────────────────────────────────────────────────────────────────┘ │ │ YES NO │ │ ▼ ▼ ✅ DynamoDB ┌────────────────────────┐ is likely a │ Consider: MongoDB Atlas│ great fit! │ (multi-cloud) or │ │ CockroachDB │ └────────────────────────┘If you're uncertain, you don't have to go all-in. Start with PostgreSQL for your primary data, then introduce DynamoDB for specific high-scale features (sessions, caching, logs) where its strengths are clear. Many successful architectures use multiple databases, each for what it does best.
The best systems often combine multiple databases, using each for its strengths. Here are proven hybrid patterns with DynamoDB:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586
// ============================================// E-commerce: Hybrid DynamoDB + PostgreSQL Architecture// ============================================ class OrderService { // PostgreSQL: Complex order data with relations private postgres: PostgresClient; // DynamoDB: High-throughput auxiliary data private dynamodb: DynamoDBClient; // ============================================ // CREATE ORDER: Use PostgreSQL for transactions // ============================================ async createOrder(orderDetails: OrderInput): Promise<Order> { // PostgreSQL transaction: order + order_items + inventory update const order = await this.postgres.transaction(async (trx) => { const order = await trx.insert("orders", orderDetails); await trx.insert("order_items", orderDetails.items); await trx.update("inventory", { decrement: orderDetails.items }); return order; }); // DynamoDB: High-throughput order status for real-time tracking await this.dynamodb.putItem({ TableName: "OrderStatus", Item: { orderId: order.id, customerId: order.customerId, status: "pending", updatedAt: new Date().toISOString() } }); return order; } // ============================================ // ORDER HISTORY: Use PostgreSQL for complex queries // ============================================ async getOrderHistory( customerId: string, filters: OrderFilters ): Promise<OrderSummary[]> { // PostgreSQL: Joins, filtering, pagination return this.postgres.query(` SELECT o.*, p.name as product_name, COUNT(oi.id) as item_count FROM orders o JOIN order_items oi ON o.id = oi.order_id JOIN products p ON oi.product_id = p.id WHERE o.customer_id = $1 AND o.created_at BETWEEN $2 AND $3 AND o.status = ANY($4) GROUP BY o.id, p.name ORDER BY o.created_at DESC LIMIT $5 OFFSET $6 `, [customerId, filters.startDate, filters.endDate, filters.statuses, filters.limit, filters.offset]); } // ============================================ // REAL-TIME STATUS: Use DynamoDB for low-latency tracking // ============================================ async getOrderStatus(orderId: string): Promise<OrderStatus> { // DynamoDB: Sub-millisecond lookup, no joins needed const result = await this.dynamodb.getItem({ TableName: "OrderStatus", Key: { orderId }, ConsistentRead: true // Ensure latest status }); return result.Item as OrderStatus; } // ============================================ // SHOPPING CART: DynamoDB (session-like, high throughput) // ============================================ async getCart(sessionId: string): Promise<Cart> { // DynamoDB: Perfect for session data with TTL const result = await this.dynamodb.getItem({ TableName: "ShoppingCarts", Key: { sessionId } }); return result.Item as Cart; }}Whether migrating to DynamoDB or away from it, understanding the challenges helps you plan realistically.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859
// Example: Migrating denormalized DynamoDB order to normalized PostgreSQL // DynamoDB item (denormalized)const dynamoOrder = { orderId: "ORD-123", customerId: "CUST-456", customerName: "Alice Smith", // Denormalized from customers customerEmail: "alice@example.com", // Denormalized from customers items: [ { productId: "PROD-001", productName: "Widget", // Denormalized from products quantity: 2, price: 49.99 }, { productId: "PROD-002", productName: "Gadget", quantity: 1, price: 99.99 } ], total: 199.97, status: "shipped", createdAt: "2024-06-15T10:30:00Z"}; // Migration to PostgreSQL (normalized)async function migrateOrder(dynamoItem: any): Promise<void> { await postgres.transaction(async (trx) => { // 1. Upsert customer (avoid duplicates) await trx.query(` INSERT INTO customers (id, name, email) VALUES ($1, $2, $3) ON CONFLICT (id) DO UPDATE SET name = $2, email = $3 `, [dynamoItem.customerId, dynamoItem.customerName, dynamoItem.customerEmail]); // 2. Insert order await trx.query(` INSERT INTO orders (id, customer_id, total, status, created_at) VALUES ($1, $2, $3, $4, $5) `, [dynamoItem.orderId, dynamoItem.customerId, dynamoItem.total, dynamoItem.status, dynamoItem.createdAt]); // 3. Insert order items for (const item of dynamoItem.items) { await trx.query(` INSERT INTO order_items (order_id, product_id, quantity, price_at_purchase) VALUES ($1, $2, $3, $4) `, [dynamoItem.orderId, item.productId, item.quantity, item.price]); } });} // Migration strategy: Stream-based for live migration// 1. Initial bulk export: DynamoDB → S3 → PostgreSQL bulk load// 2. Ongoing sync: DynamoDB Streams → Lambda → PostgreSQL writes// 3. Dual-read period: Read from both, compare, validate// 4. Cutover: Switch application to PostgreSQL, disable stream syncDatabase migrations are among the riskiest activities in software engineering. DynamoDB's unique data model makes migrations in either direction particularly challenging. Plan for months of work, extensive testing, and a rollback strategy. Consider whether the pain is justified by the destination's benefits.
Lessons from teams that have deployed DynamoDB in production—both successfully and unsuccessfully:
Successful DynamoDB deployments share a pattern: known access patterns, key-based access, operational simplicity priorities, and willingness to denormalize. Failures share a different pattern: evolving requirements, analytics needs, complex relationships, and treating DynamoDB as a general-purpose database. Learn from both.
We've explored the complete decision framework for DynamoDB. Let's consolidate the key insights:
Module Complete: Amazon DynamoDB
You've now completed a comprehensive exploration of Amazon DynamoDB—from its managed service philosophy, through partition key design and indexing, to consistency models, Streams, and decision frameworks. You understand not just how DynamoDB works, but when it's the right choice and how to use it effectively.
As you move forward to explore other real-world database architectures like Apache Cassandra and CockroachDB, you'll find both similarities (distributed systems principles) and differences (consistency models, operational approaches) that will deepen your understanding of database selection for system design.
Congratulations! You now have deep knowledge of Amazon DynamoDB—its architecture, design principles, operational model, and appropriate use cases. You can confidently evaluate whether DynamoDB fits your system, design effective schemas, implement stream processing, and avoid the common pitfalls that trap less-informed engineers.