DynamoDB Basics Explained: Tables, Keys, and Real-World Patterns
DynamoDB basics demystified — learn partition keys, sort keys, single-table design, and when to choose DynamoDB over SQL with battle-tested code examples..
20+ years shipping high-throughput database systems. Notes here come from systems that actually shipped.
- DynamoDB is a fully managed NoSQL key-value and document database with single-digit-millisecond latency at any scale
- Partition key determines which physical node stores your data; choose high-cardinality attributes to avoid hot partitions
- Sort key enables efficient range queries within a partition; composite sort keys like 'DATE#ID' unlock powerful access patterns
- Global Secondary Indexes let you query by a different key, but they add cost and eventual consistency lag
- Single-table design co-locates related entities (customers + orders) in one table to replace joins with fast partition queries
- The biggest mistake: modeling tables like SQL schemas instead of mapping access patterns first
Imagine a massive library with billions of books. Instead of organizing them alphabetically (which slows down as the library grows), you assign every book a unique locker number and go straight to that locker instantly — no browsing required. DynamoDB works the same way: it uses a 'key' to jump directly to your data in milliseconds, no matter if you have 100 rows or 100 billion. The catch? You have to decide how to label your lockers BEFORE you start filling them.
Every app eventually hits a wall with its database. SQL tables start choking on hundreds of millions of rows, joins get expensive, and scaling horizontally becomes an engineering nightmare. Amazon DynamoDB was built specifically to shatter that wall — it's the database that powers Amazon's own shopping cart, which handles millions of writes per second during Prime Day without breaking a sweat. If you're building anything at scale on AWS, DynamoDB will cross your path sooner or later.
The core problem DynamoDB solves is predictable performance at any scale. Traditional relational databases slow down as data grows because query planners scan more rows and indexes get larger. DynamoDB sidesteps this entirely by requiring you to declare your access patterns upfront. You design your keys so that every query hits exactly one partition — think of it as pre-building the fast path before traffic arrives, not scrambling to optimize after the fact.
By the end of this article you'll understand how DynamoDB's partition and sort keys actually work under the hood, how to model a real e-commerce order system without a single SQL join, why single-table design exists and when it's overkill, and the two mistakes that will silently ruin your DynamoDB performance in production.
What DynamoDB Actually Is: A Managed Key-Value Store with a Single-Table Mindset
DynamoDB is a fully managed NoSQL database that stores data as items in tables, each uniquely identified by a primary key. The core mechanic is simple: you get or put an item by its key in single-digit millisecond latency at any scale. There are no joins, no foreign keys, no secondary indexes by default. Every query must target a specific partition key — or, at best, a range of sort keys within one partition. This constraint is not a limitation; it's the design that enables predictable performance. DynamoDB achieves consistent single-digit millisecond latency by distributing data across partitions using a hash of the partition key. Provisioned throughput is divided evenly across partitions, so a hot key — one accessed far more than others — throttles requests even if the table has spare capacity. The only way to avoid this is to design your partition key for high cardinality and uniform access. On-demand capacity auto-scales but still has a per-partition throughput limit. The real power emerges when you model one-to-many and many-to-many relationships using composite sort keys, sparse indexes, and the single-table design pattern. Use DynamoDB when your workload is read- or write-heavy with predictable access patterns — user sessions, game state, IoT telemetry, or metadata caches. Avoid it when you need ad-hoc queries across multiple attributes, complex aggregations, or transactions spanning unrelated entities. In production, DynamoDB shines when you accept its constraints and model your data to match its access patterns, not when you try to make it behave like a relational database.
Partition Keys and Sort Keys: The Engine Under the Hood
Every DynamoDB table has a primary key. That key comes in two flavors: a simple primary key (just a partition key) or a composite primary key (partition key + sort key). Understanding the difference isn't just syntax trivia — it determines what queries you can run efficiently.
The partition key is hashed by DynamoDB internally to decide which physical server (partition) stores your item. This is why it's sometimes called a hash key. Every read and write for that item goes to exactly that one partition. The golden rule: items with the same partition key live on the same server. That's powerful — it means related data is co-located — but it also means if one partition key gets hit with 90% of your traffic, you have a 'hot partition' problem and performance tanks.
The sort key (also called a range key) doesn't affect which partition stores the item, but it determines the order of items within that partition. This lets you query a range — 'give me all orders for customer-42 placed in the last 30 days' — in a single, efficient request. That range query is only possible because the sort key values are stored in sorted order on disk within each partition. Think of the partition key as the drawer label in a filing cabinet, and the sort key as the alphabetical tabs inside that drawer.
Writing and Reading Data — With a Real E-Commerce Pattern
Now that the table exists, let's populate it with real data and then query it the way a production app would. The key insight here is that DynamoDB offers two fundamentally different operations: GetItem (fetch by exact primary key — always fast, O(1)) and Query (fetch all items sharing a partition key, optionally filtered by sort key — still fast because it stays within one partition). There's also Scan, which reads every item in the table — treat this like a last resort in production.
For our e-commerce example, the pattern is: partition key = customer_id, sort key = a string that combines the date and the order ID separated by a # symbol. That separator trick is deliberately chosen. Because DynamoDB sorts strings lexicographically, the ISO date format (YYYY-MM-DD) sorts chronologically. So 'give me all orders after 2024-01-01' becomes a simple sort key condition — begins_with or between — without scanning the whole table.
This is the core discipline of DynamoDB design: your sort key isn't just an ID, it's a query tool. Every character in it is a deliberate choice about what queries you want to be able to answer efficiently.
Global Secondary Indexes: Querying Data a Different Way
Here's the problem you'll hit in week two of using DynamoDB: your table is perfectly designed for 'get all orders by customer', but product management just asked for 'get all orders with status SHIPPED'. That query doesn't match your partition key at all. In SQL, you'd add an index. In DynamoDB, you add a Global Secondary Index (GSI).
A GSI is essentially a separate, automatically maintained copy of your table organized around a different key. You define a new partition key (and optionally a sort key), and DynamoDB replicates writes to that index asynchronously. Queries against a GSI are just as fast as queries against the base table — because the same partitioning logic applies.
The word 'global' means the index spans the entire table, not just one partition. There's also a Local Secondary Index (LSI), which must share the base table's partition key but can have a different sort key — and it must be defined at table creation time, not added later. GSIs can be added to existing tables, making them far more flexible in practice.
The design trade-off: every GSI costs you money (storage + write capacity) and adds a few milliseconds of eventual consistency lag. You're not getting something for free — you're choosing which query patterns deserve their own fast path.
Single-Table Design: Why One Table Often Beats Many
Single-table design (STD) is the concept that trips up virtually every developer migrating from SQL. The instinct is to create one DynamoDB table per entity — an Orders table, a Customers table, a Products table — mirroring what you'd do in a relational database. But DynamoDB wasn't designed for that, and doing it that way throws away its biggest advantage.
In SQL, you join across tables at query time. DynamoDB has no joins. Instead, single-table design stores heterogeneous entity types in the same table, using generic attribute names like PK and SK (partition key and sort key) whose values encode the entity type. For example: PK='CUSTOMER#CUST-42', SK='PROFILE' for a customer record, and PK='CUSTOMER#CUST-42', SK='ORDER#2024-03-22#ORD-002' for an order belonging to that customer. Now a single Query call with PK='CUSTOMER#CUST-42' returns the customer profile AND all their orders in one network round-trip.
This is powerful for read-heavy access patterns, but it comes with a real cost: the model is harder to reason about, harder to query ad-hoc (e.g., for analytics), and painful if your access patterns change. Single-table design is a deliberate trade: you sacrifice flexibility for performance and cost efficiency. It's the right call for a microservice with stable, well-understood access patterns. It's the wrong call for an exploratory analytics workload — use Athena or Redshift for that.
Error Handling, Retries, and the Cost of Not Planning for Throttling
DynamoDB is reliable, but it's not magic. When you exceed your provisioned throughput or hit a hot partition, DynamoDB returns ProvisionedThroughputExceededException. The SDK handles retries with exponential backoff by default — but that default can be too slow for user-facing requests. Understanding how to configure retries and handle errors is critical for production.
Every AWS SDK client includes a retry mechanism. By default, it retries up to 3 times with exponential backoff. That means a single failed request might take 1 + 2 + 4 = 7 seconds before the SDK gives up. For a customer-facing checkout, that's an unacceptable delay. You need to implement your own retry logic with jitter, and more importantly, detect the root cause — is it a burst of traffic (transient) or a sustained pattern (needs rearchitecting)?
Another silent killer: conditional writes that fail because of a stale version. If you use conditional writes for optimistic locking (e.g., update_item with ConditionExpression), a concurrent write will throw ConditionalCheckFailedException. Your code must catch that and retry the entire read-modify-write cycle. This is where the 'lost update' problem hides — never assume a conditional write succeeds.
DynamoDB also has a 1MB limit on Query/Scan results. If your query returns more than 1MB of data, you'll get paginated results via LastEvaluatedKey. Failing to paginate is a classic bug: you get the first page, assume it's complete, and silently miss data. Always check LastEvaluatedKey in a loop until it's null.
- SQL deadlock stops everything — you need to kill a transaction.
- DynamoDB throttling only affects requests to a hot partition; other partitions keep working.
- The fix for throttling is almost never to increase total capacity — it's to redistribute the load.
- Exponential backoff with jitter (rather than fixed retries) prevents thundering herd when the partition recovers.
Who This Actually Applies To — And Who Should Walk Away
DynamoDB isn't a general-purpose database. It's a hammer for a specific set of nails. If you're building an e-commerce cart, a gaming leaderboard, a session store, or an IoT telemetry pipeline — you're in the right room. If you're looking for complex joins, ad-hoc analytical queries, or ACID transactions across five tables with rollbacks — you need Postgres or Aurora, and you need to leave before the architecture astronauts convince you otherwise.
This assumes you've run a production database before. You should understand partitioning, indexes, and the difference between OLTP and OLAP. If the phrase "eventual consistency" makes you nervous, go read the DynamoDB paper first. We're not selling you a magic button. We're teaching you a tool that punishes ignorance at scale.
You should be comfortable with JSON document modeling and have a strong opinion about when denormalization makes sense. If your first instinct is "but normalization is always better," you'll fight the tool and lose. The prerequisite is humility — accepting that relational orthodoxy hurts in a key-value world.
What You Actually Need Before Touching the Console
Prerequisites aren't a checkbox exercise. They're a survival kit. First, you need a clear mental model of your access patterns — not your schema. Write them down. "Get order history for user" is a pattern. "Show me all orders from last week" is a different pattern. If you can't enumerate every query your app will make before writing a single PutItem, you're not ready.
Second, you need a basic understanding of throughput math. One RCU reads one item up to 4KB strongly consistent, or two items eventually consistent. One WCU writes one item up to 1KB. This isn't trivia — it's how you estimate your bill before your manager asks why the DevOps cost tripled.
Third, bring a realistic dataset. Not 10 records. Not 10 million. But enough to see your hot keys emerge. 100k items with skewed distribution will show you what your production 10M items look like. If you test with uniform distribution, you're testing a lie.
If you can't articulate your read-to-write ratio and your peak concurrency within 20% accuracy, go back to the whiteboard. DynamoDB punishes vague requirements with vague bills.
Advanced Features You'll Actually Use: TTL, Transactions, and Streams
DynamoDB isn't just get and put. Three advanced features separate a toy from a production system. Time To Live (TTL) lets you expire records automatically. Set a TTL attribute on your order items — DynamoDB deletes them after the timestamp passes. No cron jobs, no Lambda triggers to clean up stale data.
Transactions give you ACID across up to 25 items in a single request. Need to debit one account and credit another? Wrap both operations in a TransactWrite. If either fails, neither happens. This is your escape hatch from eventual consistency when your business demands atomicity.
DynamoDB Streams capture every write in order. Feed them to a Lambda for real-time indexing, search sync, or event-driven workflows. Combined with the Change Data Capture pattern, streams replace half your polling code. Production teams use streams to materialize views in Elasticsearch or sync to a Redshift replica without touching the source table.
Ignore these three and you're writing workarounds DynamoDB already handles.
Why Your RDBMS Reflexes Break on DynamoDB
You learned SQL joins, normalized schemas, and vertical scaling. Throw that out. DynamoDB is a different beast with different trade-offs.
In an RDBMS, you model for query flexibility. Normalize into tables, join at read time. In DynamoDB, you model for access patterns. Denormalize into a single table, fetch everything in one query. Joins don't exist. You prep your data at write time so reads are fast. That single-table design isn't a quirk — it's the point.
RDBMS scales vertically. More CPU, more RAM on one box. DynamoDB scales horizontally. One partition can handle 3000 RCU / 1000 WCU. Beyond that, it splits automatically. You don't provision for peaks — you design partition keys that distribute traffic evenly. A bad key (like a timestamp) creates a hot partition and throttled customers.
Cost model flips too. RDBMS charges per query complexity. DynamoDB charges per read/write unit. A full table scan in DynamoDB cost the same as a single-item get — both consume read capacity. Optimize for fewer, larger requests, not smaller queries.
Stop trying to make DynamoDB act like PostgreSQL. Learn its rules, or get burned.
Who Should Read This — And Who’s Wasting Their Time
This guide is built for backend engineers, senior developers, and architects designing systems where predictable latency at scale matters more than relational integrity. You’ll get the most out of DynamoDB if you’re building high-traffic e-commerce platforms, IoT event pipelines, real-time leaderboards, or session stores — any workload that demands single-digit millisecond reads and writes on terabytes of data with automatic scaling. You should stop reading now if your primary need is complex joins, ad-hoc analytical queries, multi-row transactions across five tables, or strict referential integrity enforced by a schema. Likewise, if your team is small and your database fits on a laptop, a managed relational database like PostgreSQL will save you months of cognitive overhead. DynamoDB is not a general-purpose hammer; it is a specialized power tool for access patterns you know in advance. This guide assumes you already understand partition keys, sort keys, and that dynamodb punishes scans. If you’re still writing WHERE clauses naturally, start with NoSQL fundamentals first.
Prerequisites: Tools and Concepts You Need First
Before you open the AWS console, make sure you’ve installed the AWS CLI version 2 and configured credentials with an IAM user that has dynamodb:GetItem, dynamodb:PutItem, dynamodb:Query, and dynamodb:CreateTable permissions. You should also have a local DynamoDB instance running via Docker for fast iteration without cost surprises. Conceptually, you must be comfortable with the fact that DynamoDB is schema-on-write: you define only the primary key at table creation, and any attribute can appear on any item. That means your application code enforces data shape — not the database. You also need to understand read capacity units (RCUs) and write capacity units (WCUs) as financial levers, not just technical ones. A single strongly consistent read of a 4KB item costs 1 RCU; a transactional read costs 2 RCUs. Estimate your traffic before creating a table. Finally, adopt a mindset shift: you design for known query patterns first, then build the table structure. If you cannot list your top five access patterns on a whiteboard, go back to product spec — DynamoDB will punish you for guessing.
Hot Partition Killed Our Checkout Flow on Prime Day
- Never trust PAY_PER_REQUEST to save you from a single skewed access pattern. Re-evaluate partition key cardinality before any high-traffic event.
- If you must use a low-cardinality key like customer_id for writes, implement write sharding with a random suffix and fan-out queries.
- Always test with realistic traffic distribution in a staging environment, not just uniform load.
- Set up CloudWatch alarms for ThrottledRequests per partition (available via the 'DynamoDBThrottle' metric) to catch hot partitions early.
aws dynamodb update-table --table-name Orders --provisioned-throughput WriteCapacityUnits=500 --region us-east-1aws cloudwatch get-metric-statistics --namespace AWS/DynamoDB --metric-name ThrottledRequests --dimensions Name=TableName,Value=Orders --statistics Sum --period 60 --start-time "$(date -u -d '5 minutes ago' +%Y-%m-%dT%H:%M:%SZ)"Key takeaways
Common mistakes to avoid
4 patternsChoosing a low-cardinality partition key (e.g., 'status' with only 3 values)
Using Scan instead of Query for production reads
Treating update_item like a safe partial update without condition expressions
Assuming GSIs provide strong consistency
Interview Questions on This Topic
Can you explain the difference between a partition key and a sort key, and give an example of when you'd use a composite primary key over a simple one?
Frequently Asked Questions
20+ years shipping high-throughput database systems. Notes here come from systems that actually shipped.
That's NoSQL. Mark it forged?
12 min read · try the examples if you haven't