0x55aa
← Back to Blog

DynamoDB: The NoSQL Database That Saved My E-Commerce Backend (And My Sanity) βš‘πŸ—„οΈ

β€’10 min read

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:

  1. List every query your app will make (seriously, write them all down)
  2. Choose your partition key (high cardinality, evenly distributed)
  3. Choose your sort key (enables range queries and ordering)
  4. Design for single-table (start simple, split later if needed)
  5. Plan your GSIs (for every query that isn't on the primary key)
  6. Enable Point-in-Time Recovery (free-ish, saves you from your own mistakes)
  7. Enable DynamoDB Streams (free, unlocks event-driven patterns)
  8. 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:

  1. Sign into AWS Console β†’ DynamoDB β†’ Create a table (free tier gives you 25GB!)
  2. Write down 5 access patterns your app needs
  3. Design a schema around those patterns
  4. Try single-table design with the entity prefixing pattern (USER#, ORDER#, etc.)

This month:

  1. Migrate one non-relational, high-traffic table from RDS to DynamoDB
  2. Enable DynamoDB Streams and wire up one Lambda trigger
  3. Add a GSI for your most common secondary query
  4. Compare your RDS vs DynamoDB bill and enjoy the savings πŸ’°

This quarter:

  1. Evaluate your full data model for DynamoDB candidates
  2. Build a monitoring dashboard (CloudWatch metrics are free!)
  3. Implement TTL for session data (auto-deletes expired items β€” also free!)
  4. 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! πŸ”₯