DynamoDB is a managed NoSQL database from AWS. "Managed" means you don't have to install a server, update it, or think about replication: AWS does that for you. "NoSQL" means it isn't the familiar table of rows and relationships where you can write arbitrary SQL queries, but a key-value store: you put an item in by its key and fetch it back by that same key. The defining feature of DynamoDB is that it holds a steady, low latency (single-digit milliseconds) whether you have a thousand records or billions.
But you pay for this with a change of habit. In a relational database you first design your tables "properly" (normalize them), and write queries afterward — any queries you like. In DynamoDB it's the other way around: first you have to know exactly which queries you'll run, and only then do you choose the keys for them. If you design DynamoDB "like ordinary tables," you'll either get slow, expensive scans of the whole database, or you simply won't be able to retrieve the data you need. That's why everything starts from the access model — from the list of questions your application asks of the data.
Keys: partition key and sort key
Every item (that's what DynamoDB calls a record, the equivalent of a row) has a primary key. It's made up of one or two parts.
Partition key — the mandatory part. DynamoDB uses it to decide which physical "chunk" of storage (partition) the item will live on. Picture a library with many shelving units: the partition key is the unit number. It determines how evenly the load spreads out. A bad key — one with few distinct values, say, or one value that shows up far more often than the rest — creates a "hot" partition: one unit is besieged by readers while the others sit empty, and it becomes a bottleneck.
Sort key — an optional second part. If the partition key is the unit number, then the sort key is the order of the books on the shelf. It orders items within a single partition and lets you run range queries: "all of a user's orders for March," "everything that starts with ORDER#." For that there are operators like begins_with and between.
The main read operation is Query. It works fast, but only by key: "give me the items with this partition key and a sort key in this range." Anything not by key is done through Scan — a full pass over the entire table. Scan is expensive and slow, and you avoid it. Hence the beginner's rule: first write out all of your application's queries, then choose keys so that every query lands in a Query.
aws dynamodb query \
--table-name Orders \
--key-condition-expression "pk = :u AND begins_with(sk, :prefix)" \
--expression-attribute-values '{":u":{"S":"USER#42"},":prefix":{"S":"ORDER#"}}'
Indexes: GSI and LSI
Often you need to look up data by something other than the primary key. For example, orders are stored under USER#42, but sometimes you need to find an order by its status. For these extra lookup paths you set up secondary indexes — automatically maintained copies of the data with a different key.
GSI (Global Secondary Index) — a separate index with its own partition key and sort key. It has its own throughput, independent of the table. A GSI can be added at any point in a table's life — handy when a new lookup scenario appears. An important limitation: reads from a GSI are always eventually consistent (more on that below), and you build that into the design.
LSI (Local Secondary Index) — an index with the same partition key as the table but a different sort key. It has three differences from a GSI worth memorizing: it can be created only at the moment the table is created (you can't add or drop it later), it shares throughput with the table, and for a single partition key value the total data volume including the LSI is capped at 10 GB. In return, an LSI can serve strongly consistent reads.
aws dynamodb create-table \
--table-name Orders \
--attribute-definitions AttributeName=pk,AttributeType=S AttributeName=status,AttributeType=S \
--key-schema AttributeName=pk,KeyType=HASH \
--global-secondary-indexes '[{"IndexName":"by-status","KeySchema":[{"AttributeName":"status","KeyType":"HASH"}],"Projection":{"ProjectionType":"ALL"}}]' \
--billing-mode PAY_PER_REQUEST
In practice, the GSI is the main tool for extra access paths, because it's more flexible to add and scale. Each index is a copy of the data (you can choose which attributes to project into it), and you pay for it in storage and writes. So you set up indexes strictly for a specific query, not "just in case it comes in handy."
Billing modes: on-demand and provisioned
DynamoDB charges for throughput — for reads and writes. There are two modes.
On-demand (PAY_PER_REQUEST) — you pay per request as it happens, and capacity adapts to the load instantly and on its own. This is the simplest start: nothing to configure, no volume to guess. It suits a new project and uneven, spiky load.
Provisioned — you set the number of read and write units per second in advance (you can turn on auto-scaling so they change on a schedule). With steady, predictable load this is noticeably cheaper, because you pay for what's reserved rather than for every request.
The typical path: start with on-demand, and once the load settles and the profile becomes clear, switch to provisioned for the savings. For more on counting the money in the cloud, see the piece on cost optimization.
Consistency and single-table design
Consistency is about how fresh the data you see is right after a write. In DynamoDB a read is eventually consistent by default: it's cheaper, but right after a write it may for a fraction of a second return a slightly stale value while the change propagates across the copies. If you need a guarantee of the freshest data, you explicitly ask for a strongly consistent read — it's more expensive and a bit slower. Remember: reads from a GSI are always eventually consistent only, there's no choice there.
Single-table design is an advanced technique where different entity types (orders and their line items, say) are put into a single table so that one query pulls the related data all at once. It's powerful and saves round-trips to the database, but designing it is hard. For many services it's simpler and clearer to keep several separate tables. It's a deliberate choice made for load, not a mandatory practice — a beginner should almost always start with a few simple tables.
When DynamoDB, and when a relational database
The main fork worth stating out loud — DynamoDB and a relational database (RDS) solve different classes of problems, and neither is "better."
DynamoDB shines when the access patterns are known in advance and stable, the queries are simple and go by key, and the scale is large with a requirement for predictable latency. Typical examples: user sessions, profiles, event history, shopping carts, a notifications feed.
RDS (PostgreSQL, MySQL, and the like) is what you need when the queries are complex: joins across several tables, aggregates and reports, transactions touching several entities at once, and situations where the way you access the data changes over time and you want the flexibility of SQL. If you don't yet know which queries you'll need, a relational database forgives that better.
A simple rule: if you know your queries up front and there aren't many of them, and you need scale — take DynamoDB and design the keys around the queries; if you need flexibility and complex relationships — take a relational database.
Where this shows up
DynamoDB appears in almost any AWS application that has "hot" data with a clear key-based access pattern and heavy load: storing sessions, profiles, feature flags, carts, event feeds. It fits especially naturally into serverless architectures on Lambda, where you don't keep a server running all the time and the database has to scale itself and be billed per use.
Typical beginner mistakes: designing a table "like in SQL" and then hitting Scan on every screen; picking a partition key with few distinct values and getting a hot partition; adding a pile of GSIs "just in case" and paying for them; forgetting that an LSI can't be added after the table is created; expecting strongly consistent reads from a GSI, which it doesn't have; diving straight into single-table design without a real need.
What to learn next: go through the neighboring managed AWS databases to see the full palette of storage options; work through the AWS fundamentals and serverless, where DynamoDB is used most often; take a look at object storage — that's a different kind of data (files, backups), and it's useful to understand the boundary between them. It's also worth looking at the AWS Well-Architected Framework — there the choice of storage for the task is covered as part of the engineering principles.