DynamoDB: The NoSQL Database That Will Either Save Your Life or Ruin Your Weekend ā”šļø
DynamoDB: The NoSQL Database That Will Either Save Your Life or Ruin Your Weekend ā”šļø
Real talk: The first time I used DynamoDB, I thought it was just "S3 but for database rows." I was wrong. SO wrong. I designed my first table like a MySQL database, ran a query, and received a response that said I'd scanned 5 million items to find 3 records.
The AWS bill that month was... educational. š¬
Welcome to DynamoDB - where the performance is incredible, the scalability is legendary, and the beginner mistakes cost real money!
What's DynamoDB, Actually? š¤
Think of DynamoDB like a super-powered filing cabinet:
- SQL database: Flexible folders, can find anything by searching content
- DynamoDB: Rigid labeled drawers, BLAZING fast if you know the label, expensive nightmare if you don't
DynamoDB hierarchy:
Table ā Items (rows) ā Attributes (columns, but flexible!)
Your data: { userId: "u123", orderId: "o456", status: "shipped", total: 99.99 }
Why it's incredible:
- Zero servers to manage (100% serverless!)
- Scales from 0 to millions of requests/second automatically
- Single-digit millisecond latency (consistently!)
- No capacity planning headaches
Why it humbles you:
- No JOINs (relational thinking = suffering)
- Limited query patterns (must plan data access upfront!)
- Scans are expensive and slow
- The learning curve is a cliff, not a ramp š§
The Partition Key: Everything Depends On This šļø
In production, I've deployed a DynamoDB-backed e-commerce system handling thousands of orders per minute. The single most important decision? The partition key.
DynamoDB distributes your data across partitions using the partition key like a hash. Choose wrong, and you get "hot partitions" - one overloaded server while the rest sit idle.
Bad partition key choice:
// Table: Orders
// Partition Key: status (e.g., "pending", "shipped", "delivered")
// What happens:
// 90% of orders are "delivered" ā ONE partition does all the work
// "pending" partition: crickets š¦
// "delivered" partition: š„š„š„ (throttled, slow, expensive!)
Good partition key choice:
// Partition Key: userId
// Sort Key: orderId (for ordering within a user's data)
// What happens:
// User u001's orders ā partition A
// User u002's orders ā partition B
// User u003's orders ā partition C
// Even distribution = happy DynamoDB! ā
The golden rule: Pick a partition key with HIGH CARDINALITY (many unique values). User IDs, product IDs, UUIDs - great! Status fields, booleans, category names - terrible! šÆ
When I architected our checkout system, I made customerId the partition key and timestamp#orderId the sort key. Queries like "get all orders for customer X in the last 30 days" became instant. No full table scans. No $300 AWS bills from bad queries! š°
Single Table Design: The Mind-Bending Pattern That Changes Everything š¤Æ
Here's the pattern that blew my mind - Single Table Design.
Instead of one table per entity (like SQL), put EVERYTHING in ONE table:
// Traditional SQL thinking (wrong for DynamoDB):
// - Users table
// - Orders table
// - Products table
// - Reviews table
// DynamoDB Single Table Design:
// - ONE table with a generic PK/SK
// Users:
{ PK: "USER#u123", SK: "PROFILE", name: "Alice", email: "[email protected]" }
// Orders for that user:
{ PK: "USER#u123", SK: "ORDER#2026-03-01", total: 49.99, status: "shipped" }
{ PK: "USER#u123", SK: "ORDER#2026-03-10", total: 129.00, status: "pending" }
// Product catalog:
{ PK: "PRODUCT#p1", SK: "METADATA", name: "Widget", price: 9.99 }
{ PK: "PRODUCT#p1", SK: "REVIEW#r001", rating: 5, comment: "Great!" }
Why this works:
- Query a user + all their orders in ONE request (no JOINs needed!)
- Pay for one table, get multiple "logical tables" for free
- Access patterns are fast and predictable
A serverless pattern that saved us: Fetching a user's profile + last 5 orders used to be 2 SQL queries. With Single Table Design in DynamoDB, it's ONE query with a range condition on the sort key. Cut our Lambda execution time in half! ā”
The Access Pattern Trap (Plan Before You Build) š
Here's the brutal truth about DynamoDB: you MUST know your access patterns before designing your schema. This is the exact opposite of SQL, where you normalize first and write queries later.
My planning checklist for every table:
Access patterns for our e-commerce backend:
1. Get user by ID ā PK: USER#userId
2. Get all orders for a user ā PK: USER#userId, SK starts_with "ORDER#"
3. Get order by ID ā PK: ORDER#orderId (separate lookup!)
4. Get all products in a category ā GSI: categoryId + price
5. Get product by ID ā PK: PRODUCT#productId
The Global Secondary Index (GSI) is your escape hatch:
// Can't query by status on your main table?
// Add a GSI!
// GSI: OrdersByStatus
// GSI PK: status
// GSI SK: createdAt
// Now you can query:
const pendingOrders = await dynamodb.query({
TableName: 'EcommerceTable',
IndexName: 'OrdersByStatus',
KeyConditionExpression: '#status = :s AND createdAt > :date',
ExpressionAttributeNames: { '#status': 'status' },
ExpressionAttributeValues: {
':s': 'pending',
':date': '2026-03-01T00:00:00Z'
}
}).promise();
GSI gotcha: GSIs cost extra. Each GSI replicates data = more storage + write costs. Don't add 10 GSIs just because you can! Each one should justify its existence! šø
Cost Gotchas That Burned Me š„šø
DynamoDB pricing looks simple until it isn't:
Gotcha #1: Table Scans Are Budget Destroyers
// BAD - Full table scan (reads EVERY item!)
const allOrders = await dynamodb.scan({
TableName: 'Orders',
FilterExpression: 'status = :s',
ExpressionAttributeValues: { ':s': 'pending' }
}).promise();
// Scanned 5M items, found 200 ā billed for 5M read units! š±
// GOOD - Targeted query (reads only what you need!)
const pendingOrders = await dynamodb.query({
TableName: 'Orders',
IndexName: 'StatusIndex',
KeyConditionExpression: '#status = :s',
ExpressionAttributeValues: { ':s': 'pending' }
}).promise();
// Read 200 items ā billed for 200 read units! ā
Real cost difference: I once changed a weekly reporting job from Scan to Query. Monthly DynamoDB bill dropped from $180 ā $12. Same data, smarter access! šÆ
Gotcha #2: On-Demand vs Provisioned Capacity
On-Demand Mode:
- Pay per request (simple!)
- Great for unpredictable traffic
- More expensive at high, steady traffic
- ~$1.25 per million writes, $0.25 per million reads
Provisioned Mode:
- Reserve Read/Write Capacity Units (RCUs/WCUs)
- Cheaper at predictable, steady traffic
- Auto Scaling available (use it!)
- Risk: provision too low ā throttling; too high ā wasted money
My production setup:
- Development tables: On-Demand (traffic unpredictable, keep it simple)
- Production tables: Provisioned + Auto Scaling (predictable traffic, 40% cheaper!)
A cost optimization that saved us: Switched our main product catalog table from On-Demand to Provisioned after 3 months of stable traffic data. Saved $85/month with zero performance change! š°
Gotcha #3: DynamoDB Streams Bill You Separately
// Enabling DynamoDB Streams to trigger Lambda on changes:
// Table: $0.02 per 100K writes
// Streams: ALSO $0.02 per 100K reads from the stream
// Lambda: $0.20 per million invocations
// For 10M writes/month:
// Table writes: $2
// Stream reads: $2
// Lambda: $2
// Total: $6 (not bad! But easy to forget the stream cost)
In production, I've deployed a real-time inventory sync using DynamoDB Streams ā Lambda ā ElastiCache. Works beautifully. But when traffic spiked during a sale, stream processing costs tripled. Always set billing alerts! šØ
Common Pitfalls to Avoid šŖ¤
Pitfall #1: Storing Large Items
DynamoDB has a 400KB limit per item. Sounds fine until you store JSON blobs:
// BAD - Product with embedded reviews
{
productId: "p123",
name: "Widget",
reviews: [/* 500 reviews Ć 500 bytes = 250KB! Getting close to limit */]
}
// GOOD - Store reviews separately
// Product item: { PK: "PRODUCT#p123", SK: "METADATA", name: "Widget" }
// Review items: { PK: "PRODUCT#p123", SK: "REVIEW#r001", text: "...", rating: 5 }
Pitfall #2: Using DynamoDB for Aggregations
// BAD - Counting total orders per user (never do this!)
const scanResult = await dynamodb.scan({
TableName: 'Orders',
FilterExpression: 'userId = :u',
ExpressionAttributeValues: { ':u': 'u123' }
}).promise();
const count = scanResult.Items.length;
// Scans EVERYTHING. Don't.
// GOOD - Maintain a counter attribute
await dynamodb.update({
TableName: 'Users',
Key: { PK: 'USER#u123', SK: 'PROFILE' },
UpdateExpression: 'ADD orderCount :inc',
ExpressionAttributeValues: { ':inc': 1 }
}).promise();
// O(1) reads forever!
Pitfall #3: Forgetting Conditional Writes
// BAD - Race condition! Two Lambdas update stock simultaneously
await dynamodb.update({
TableName: 'Products',
Key: { PK: 'PRODUCT#p123' },
UpdateExpression: 'SET stock = stock - :qty',
ExpressionAttributeValues: { ':qty': 1 }
}).promise();
// Two concurrent requests ā stock goes -1! Oversell disaster! š±
// GOOD - Conditional expression prevents race condition
await dynamodb.update({
TableName: 'Products',
Key: { PK: 'PRODUCT#p123' },
UpdateExpression: 'SET stock = stock - :qty',
ConditionExpression: 'stock >= :qty',
ExpressionAttributeValues: { ':qty': 1 }
}).promise();
// One request succeeds, other gets ConditionalCheckFailedException ā
When architecting on AWS, I learned: Conditional writes are DynamoDB's answer to transactions for simple cases. Use them! They saved our inventory system from overselling during flash sales! š
When DynamoDB is the Right Choice ā
Use DynamoDB when:
- You have clear access patterns
- Traffic is unpredictable (scales to zero AND to millions!)
- You need single-digit millisecond latency
- Your app is serverless (Lambda + DynamoDB = perfect pair)
- Items are self-contained (user profile, order, product)
Don't use DynamoDB when:
- You need complex ad-hoc queries (use RDS/Aurora instead)
- You have heavy reporting/analytics (use Redshift or Athena)
- Your team is SQL-only and can't learn the new paradigm
- Your data is highly relational (graph-like) with many JOINs
My production stack for e-commerce:
- DynamoDB: Orders, cart, sessions, user preferences ā
- RDS Aurora: Product catalog (complex filtering needs SQL) ā
- ElastiCache: Hot product data, session tokens ā
- Mix and match! Use DynamoDB where it shines, not everywhere!
The Quick Reference Cheat Sheet š
DO: DON'T:
ā
Use Query (targeted) ā Use Scan (reads everything)
ā
High-cardinality partition key ā Low-cardinality PK (status, bool)
ā
Single Table Design ā One table per entity (SQL thinking)
ā
Plan access patterns first ā Design schema then figure out queries
ā
Use Conditional writes ā Update without conditions (race conditions!)
ā
Enable TTL for expiring data ā Manually delete expired items (costs writes)
ā
Use projections in GSIs ā Store all attributes in GSIs (costs storage)
TL;DR - The Bottom Line š”
DynamoDB is genuinely amazing when used correctly. In production, our serverless e-commerce backend handles Black Friday traffic spikes (10Ć normal) with zero configuration changes, zero server management, and predictable costs.
But you MUST respect it:
- Plan access patterns before you write a line of code (seriously!)
- Never, EVER scan in production (unless you enjoy pain and AWS bills)
- Single Table Design - weird at first, incredible once you get it
- On-Demand for dev, Provisioned for prod (40% savings at scale!)
- Conditional writes for anything involving inventory or counters
The learning curve is real. I spent a full weekend redesigning a table I'd built "wrong" before. But once DynamoDB clicks? You'll wonder how you ever worried about database scaling. āļøā”
Your Action Plan šÆ
This week:
- Read Alex DeBrie's DynamoDB Guide - best free resource out there
- List your top 5 query patterns BEFORE designing your first table
- Build a toy project using Single Table Design
This month:
- Enable DynamoDB TTL on any time-expiring data (sessions, caches)
- Check if any production Scan operations exist ā convert to Query
- Add Auto Scaling to provisioned tables
This quarter:
- Review GSIs - are they all pulling their weight?
- Analyze your RCU/WCU usage patterns (optimize capacity mode!)
- Become the DynamoDB expert your team didn't know they needed! š
Still designing DynamoDB tables like MySQL? Connect with me on LinkedIn - I've made every DynamoDB mistake so you don't have to!
Want to see my Single Table Design templates? Check out my GitHub - real e-commerce schema examples!
Now go forth and stop scanning your tables! ā”šļø
P.S. DynamoDB's free tier is 25GB storage + 25 WCU + 25 RCU per month - FOREVER. That's enough for a real side project with zero cost. No excuses for not experimenting! š
P.P.S. I once got a $400 DynamoDB bill from a Lambda that ran a Scan in a loop. Set billing alerts at $10, $50, and $100. Learn from my pain. šØšø