DynamoDB: The NoSQL Database That Saved My E-Commerce Backend (And My Sanity) β‘ποΈ
DynamoDB: The NoSQL Database That Saved My E-Commerce Backend (And My Sanity) β‘ποΈ
Real talk: I spent the first three years of my career absolutely convinced that every problem was a PostgreSQL problem. Relational databases, ACID transactions, beautiful JOINs β I was a believer. Then I deployed an e-commerce backend to handle a flash sale, watched my RDS instance curl up and die at 12,000 concurrent users, and had a very bad Monday. π
That was the day I met DynamoDB. We had a rocky start. We've since become inseparable.
Welcome to the database that laughs at your traffic spikes and charges you for the privilege β but in the best possible way!
What Even IS DynamoDB? π€
Think of DynamoDB like a very opinionated filing cabinet:
- SQL database: "Here's a flexible cabinet. Put anything anywhere! JOIN drawers together! Run complex queries!"
- DynamoDB: "Here's a hyper-optimized cabinet. Tell me EXACTLY where you'll look for things, and I'll retrieve them in single-digit milliseconds at ANY scale. But don't you dare do a table scan."
Translation: DynamoDB trades query flexibility for infinite horizontal scale and consistent single-digit millisecond latency β even at millions of requests per second.
In production, I've deployed DynamoDB tables handling 50,000+ reads/second for product catalog lookups during Black Friday. The database didn't blink. My RDS bill did. πΈ
The Moment DynamoDB Saved Us π
Black Friday. 2 AM. Flash sale goes live. Traffic goes from 200 req/sec to 8,000 req/sec in 90 seconds.
Old world (RDS PostgreSQL):
02:01:00 - Connections spiking
02:01:45 - Connection pool exhausted
02:02:10 - CPU at 100%
02:02:30 - Queries timing out
02:02:45 - 500 errors everywhere
02:03:00 - Me, crying, refreshing PagerDuty
New world (DynamoDB + Lambda):
02:01:00 - Traffic spikes
02:01:00 - DynamoDB autoscales
02:01:00 - Still returning results in 4ms
02:01:00 - Lambda scales to handle concurrent requests
02:01:00 - Me, asleep, unaware
A serverless pattern that saved us: DynamoDB + Lambda scale together. No connection pools. No CPU bottlenecks. Just AWS routing your requests to as many servers as needed, silently, automatically. π―
The Most Important Concept: Access Patterns First πΊοΈ
This is the #1 thing that tripped me up coming from SQL.
SQL thinking:
-- Design tables, query whatever you want later
SELECT u.name, o.total, p.name
FROM users u
JOIN orders o ON u.id = o.user_id
JOIN products p ON o.product_id = p.id
WHERE u.email = '[email protected]'
ORDER BY o.created_at DESC;
DynamoDB thinking:
"I need to fetch a user's recent orders. That's my access pattern.
Design my table around that. No JOINs. One table. One query."
When architecting on AWS, I learned: Design your DynamoDB schema by listing EVERY query your app will ever make. Then work backwards to your keys. Fighting this rule is how people end up with $4,000/month DynamoDB bills from full table scans. π¬
The Key Concepts (That Actually Make Sense) π
Partition Key + Sort Key
Table: Orders
βββββββββββββββββββββββββββββββββββββββββββββ
PK (Partition Key) | SK (Sort Key) | Data
βββββββββββββββββββββββββββββββββββββββββββββ
USER#usr_123 | ORDER#2026-03-07 | { total: 99.99, status: "shipped" }
USER#usr_123 | ORDER#2026-03-06 | { total: 49.99, status: "delivered" }
USER#usr_456 | ORDER#2026-03-07 | { total: 199.00, status: "pending" }
PRODUCT#prod_789 | META | { name: "Widget", stock: 42 }
What this gives you:
- Fetch all orders for
usr_123? One query. Blazing fast. β - Fetch a specific order? Even faster. β
- Fetch orders between two dates? Easy with sort key range queries. β
- "Show me all orders above $100 across all users"? ... We'll talk about that. β οΈ
The Single Table Design
In production, I've deployed entire e-commerce backends on ONE DynamoDB table. Orders, products, users, sessions β everything. Sounds insane. It's actually brilliant:
// One table, many entity types
const params = {
TableName: 'ecommerce-prod',
Key: {
PK: 'USER#usr_123',
SK: 'PROFILE'
}
};
// Also works for orders
const orderParams = {
TableName: 'ecommerce-prod',
Key: {
PK: 'USER#usr_123',
SK: 'ORDER#ord_456'
}
};
Why single table? Fewer tables = fewer costs. Related items can live on the same partition = fewer round trips. Access patterns stay fast. π
Real Code: What CRUD Actually Looks Like π»
const { DynamoDBClient, GetItemCommand, PutItemCommand, QueryCommand } = require('@aws-sdk/client-dynamodb');
const { marshall, unmarshall } = require('@aws-sdk/util-dynamodb');
const client = new DynamoDBClient({ region: 'us-east-1' });
// Get a user's profile
async function getUser(userId) {
const command = new GetItemCommand({
TableName: 'ecommerce-prod',
Key: marshall({
PK: `USER#${userId}`,
SK: 'PROFILE'
})
});
const response = await client.send(command);
return response.Item ? unmarshall(response.Item) : null;
}
// Get all orders for a user (last 30 days)
async function getUserOrders(userId) {
const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString();
const command = new QueryCommand({
TableName: 'ecommerce-prod',
KeyConditionExpression: 'PK = :pk AND SK BETWEEN :start AND :end',
ExpressionAttributeValues: marshall({
':pk': `USER#${userId}`,
':start': `ORDER#${thirtyDaysAgo}`,
':end': 'ORDER#~' // ~ sorts after all dates
})
});
const response = await client.send(command);
return response.Items?.map(unmarshall) ?? [];
}
Clean. Fast. No connection pool. No ORM magic. Just targeted queries that return in milliseconds. π―
Cost: Where DynamoDB Gets Interesting π°
Two pricing modes:
On-Demand (what I started with):
Read: $0.25 per million requests
Write: $1.25 per million requests
No capacity planning. No "I provisioned too many units and wasted money." Pay for exactly what you use. Perfect for startups and unpredictable traffic.
Provisioned (what I moved to at scale):
Read: $0.00013 per RCU/hour
Write: $0.00065 per WCU/hour
You commit to a capacity level. Way cheaper if your traffic is predictable. Enable Auto Scaling and it adjusts within your min/max limits automatically.
My actual production bill:
- 10 million reads/day + 2 million writes/day
- On-demand: ~$85/month
- Provisioned (with auto scaling): ~$28/month
- RDS equivalent (with Multi-AZ): ~$200/month
That's 70% cheaper than RDS for my workload! And it outperforms it. π€
Pitfalls That Cost Me Real Money πͺ€
Pitfall #1: Hot Partitions
// BAD - All writes go to the SAME partition
await put({ PK: 'ORDERS', SK: Date.now() });
// Every write hits one server. Throttled. Slow. Expensive.
// GOOD - Spread writes across partitions
await put({ PK: `ORDER#${userId}`, SK: Date.now() });
// Writes are distributed. Fast. Efficient.
Lesson: Never use a single value as your partition key for high-write tables. Distribute the load!
Pitfall #2: Scans in Production
// THIS WILL RUIN YOUR LIFE (and budget)
const command = new ScanCommand({
TableName: 'ecommerce-prod',
FilterExpression: 'orderStatus = :status',
ExpressionAttributeValues: marshall({ ':status': 'pending' })
});
// Reads EVERY item in the table. Charges you for every read. πΈ
Fix: Use a Global Secondary Index (GSI):
// Create GSI on orderStatus field, then query it
const command = new QueryCommand({
TableName: 'ecommerce-prod',
IndexName: 'orderStatus-index',
KeyConditionExpression: 'orderStatus = :status',
ExpressionAttributeValues: marshall({ ':status': 'pending' })
});
// Only reads relevant items. Fast. Cheap. β
In production, I've deployed tables where a single accidental scan cost $12 in one API call. GSIs exist for a reason. Use them!
Pitfall #3: Forgetting DynamoDB Streams for Events
One of the most powerful (and underused) DynamoDB features:
// serverless.yml - trigger Lambda on every DynamoDB change
functions:
processOrderChange:
handler: handlers/orderProcessor.handler
events:
- stream:
type: dynamodb
arn: !GetAtt OrdersTable.StreamArn
batchSize: 10
startingPosition: LATEST
What this enables:
- Order placed β Stream event β Lambda sends confirmation email
- Product stock updated β Stream event β Lambda notifies waitlisted users
- User profile changed β Stream event β Lambda syncs to analytics
A serverless pattern that saved us: DynamoDB Streams + Lambda replaced an entire message queue system. Zero additional infrastructure. Zero extra cost (streams are free!). π
When NOT to Use DynamoDB β
I'll be honest β DynamoDB isn't always the answer:
Don't use DynamoDB when:
- You need complex JOINs across many entity types (β use RDS/Aurora)
- You have complex reporting queries (β use Redshift or Aurora)
- Your data is highly relational with many foreign keys (β use PostgreSQL)
- Your team has zero NoSQL experience and a tight deadline (β RDS is fine!)
- You need full-text search (β use OpenSearch/Elasticsearch alongside DynamoDB)
When architecting on AWS, I learned: DynamoDB is incredible for high-throughput, predictable-access-pattern workloads. It's miserable for ad-hoc analytics. Know your use case!
My production setup:
- DynamoDB: Product catalog, shopping carts, sessions, order history
- RDS Aurora: Financial transactions, inventory management, reporting
- ElastiCache: Hot product data, API rate limiting
Use the right tool. Don't go "all DynamoDB" any more than you'd go "all PostgreSQL." π οΈ
The DynamoDB Starter Checklist β
Before you design your first table:
- List every query your app will make (seriously, write them all down)
- Choose your partition key (high cardinality, evenly distributed)
- Choose your sort key (enables range queries and ordering)
- Design for single-table (start simple, split later if needed)
- Plan your GSIs (for every query that isn't on the primary key)
- Enable Point-in-Time Recovery (free-ish, saves you from your own mistakes)
- Enable DynamoDB Streams (free, unlocks event-driven patterns)
- Start with On-Demand pricing (switch to provisioned once you know your traffic)
# Create a table with all the good stuff enabled
aws dynamodb create-table \
--table-name ecommerce-prod \
--billing-mode PAY_PER_REQUEST \
--attribute-definitions \
AttributeName=PK,AttributeType=S \
AttributeName=SK,AttributeType=S \
--key-schema \
AttributeName=PK,KeyType=HASH \
AttributeName=SK,KeyType=RANGE \
--stream-specification StreamEnabled=true,StreamViewType=NEW_AND_OLD_IMAGES \
--point-in-time-recovery-specification PointInTimeRecoveryEnabled=true
The Bottom Line π‘
DynamoDB is not a replacement for relational databases. It's a different animal entirely β one built specifically for the kind of traffic that makes your RDS instance give up and go home.
My honest take after years of production AWS experience:
- For high-traffic, read-heavy, predictable-access workloads: DynamoDB is unbeatable.
- For complex relational data with changing query requirements: stick with RDS.
- For serverless applications on AWS: DynamoDB is your best friend.
When I architected the e-commerce backend handling our flash sales, switching from RDS to DynamoDB wasn't just a performance win β it was a cost win, an ops win, and a "sleep well on Black Friday" win. π
The learning curve is real. The partition key confusion is real. The "oh god what do you mean I can't just query by email address" frustration is real. But once it clicks? You'll never look at a traffic spike the same way again.
DynamoDB doesn't sweat. And with the right schema design, neither will you. π
Your Action Plan π―
This week:
- Sign into AWS Console β DynamoDB β Create a table (free tier gives you 25GB!)
- Write down 5 access patterns your app needs
- Design a schema around those patterns
- Try single-table design with the entity prefixing pattern (USER#, ORDER#, etc.)
This month:
- Migrate one non-relational, high-traffic table from RDS to DynamoDB
- Enable DynamoDB Streams and wire up one Lambda trigger
- Add a GSI for your most common secondary query
- Compare your RDS vs DynamoDB bill and enjoy the savings π°
This quarter:
- Evaluate your full data model for DynamoDB candidates
- Build a monitoring dashboard (CloudWatch metrics are free!)
- Implement TTL for session data (auto-deletes expired items β also free!)
- Become the DynamoDB guru on your team! π
Still on the fence about DynamoDB? Connect with me on LinkedIn and tell me your access patterns β I'll tell you if NoSQL is right for you!
Want to see my DynamoDB schema patterns? Check out my GitHub β real single-table design examples from production!
Now go forth and partition wisely! β‘ποΈ
P.S. The most common DynamoDB question I get: "Can I use auto-increment IDs?" No. Use UUIDs. Embrace it. Your partition key will thank you. π
P.P.S. I once did a table scan in production "just to check something quickly." It read 4.2 million items and cost $1.05. I have never done a table scan since. Learn from my pain! π₯