Introduction to Amazon DynamoDB: Tables, Indexes, and Provisioned Capacity

Introduction to Amazon DynamoDB: Tables, Indexes, and Provisioned Capacity

When I migrated my first application off a self-managed database cluster and onto DynamoDB, the thing that struck me most wasn’t the performance — it was how much operational work simply disappeared. No patching, no replication setup, no manual failover scripts. But that convenience came with a real learning curve, because DynamoDB asks you to think about data very differently than almost any database I’d used before. In this article, I’ll cover what DynamoDB actually is, how tables and indexes work, and how capacity and pricing models fit into the picture.

What Is DynamoDB?

DynamoDB is Amazon Web Services’ fully managed, serverless NoSQL key-value and document database. It was built to address the same class of problems that inspired Cassandra — the original Dynamo paper from Amazon is a shared ancestor of both systems — but as an AWS-managed service, DynamoDB removes essentially all the infrastructure management that comes with running your own distributed database cluster. AWS handles replication, sharding, hardware provisioning, patching, and failover behind the scenes.

DynamoDB is designed for single-digit millisecond latency at any scale, whether your table holds a few hundred rows or tens of billions. That consistency of performance regardless of scale is one of its defining characteristics, and it’s achieved through the same fundamental principle that governs Cassandra: your data model needs to be built around your access patterns, not the other way around.

Tables, Items, and Attributes

DynamoDB’s terminology maps roughly to concepts you may already know, but with different names. A table is a collection of data, similar to a table in a relational database or a collection in MongoDB. An item is a single record within a table, analogous to a row. Each item is made up of attributes, which are analogous to columns — but unlike a relational table, items in the same DynamoDB table don’t need to share the same set of attributes. This schema flexibility at the item level is one of DynamoDB’s more document-database-like qualities, even though it’s fundamentally a key-value store at its core.

Every table requires a primary key, which DynamoDB uses to distribute and organize data internally. There are two forms this primary key can take.

Simple Primary Key (Partition Key Only)

With a simple primary key, you specify just a partition key, and every item must have a unique value for that attribute. DynamoDB hashes this partition key value to determine which physical partition the item is stored on. This works well when you always look up items by a single unique identifier, like a user_id or order_id.

Composite Primary Key (Partition Key + Sort Key)

A composite primary key combines a partition key with a sort key. Together, the pair must be unique, but you can have many items sharing the same partition key as long as their sort keys differ. This is enormously powerful — it lets you group related items together under one partition key and retrieve them efficiently, sorted by the sort key, in a single query.

For example, in an orders table, you might use customer_id as the partition key and order_date as the sort key. This lets you efficiently fetch “all orders for customer X, sorted by date” — all items sharing that partition key live together and can be queried as a contiguous range.

Indexes: Local and Global Secondary Indexes

The primary key gives you fast, efficient access along one specific path, but real applications usually need to query data multiple ways. This is where secondary indexes come in.

Local Secondary Indexes (LSI)

A local secondary index lets you query using the same partition key as your base table, but with a different sort key. LSIs must be created at the same time as the table (you can’t add one later), and they share the read/write capacity of the base table’s partition. They’re useful when you need a different sort order within the same partition — for instance, sorting a customer’s orders by total_amount instead of order_date, while still partitioning by customer_id.

Global Secondary Indexes (GSI)

A global secondary index is far more flexible — it lets you define an entirely different partition key (and optionally a different sort key) than the base table. GSIs can be added or removed at any time after table creation, and they have their own provisioned capacity, separate from the base table. This makes GSIs the tool I reach for most often when I need to support a genuinely different access pattern — for example, querying orders by product_id instead of customer_id, using a GSI with product_id as its partition key.

A key nuance with GSIs: they’re eventually consistent by default (writes to the base table propagate to the index asynchronously), while LSIs support strongly consistent reads if needed. This is an important detail to account for if your application logic depends on reading your own writes immediately.

Provisioned Capacity vs. On-Demand Capacity

DynamoDB offers two capacity modes, and choosing between them is one of the first real decisions you’ll make with any table.

Provisioned Capacity

In provisioned mode, you specify Read Capacity Units (RCUs) and Write Capacity Units (WCUs) ahead of time. One RCU supports one strongly consistent read per second (or two eventually consistent reads) of an item up to 4KB; one WCU supports one write per second of an item up to 1KB. You pay for the capacity you provision, regardless of whether you fully use it, but you can also enable auto-scaling, which adjusts provisioned capacity within bounds you set, based on actual traffic.

Provisioned capacity is generally more cost-effective for predictable, steady workloads, since you’re essentially reserving capacity at a lower unit rate than on-demand pricing.

On-Demand Capacity

On-demand mode removes capacity planning entirely — DynamoDB automatically scales to handle whatever traffic you throw at it, and you pay per request actually made, rather than for pre-provisioned throughput. This is fantastic for unpredictable or spiky workloads, new applications where you don’t yet know your traffic patterns, or workloads with very sporadic activity.

The tradeoff is cost: on-demand pricing per request is meaningfully higher than the equivalent provisioned throughput cost, so a steady, well-understood, high-volume workload will usually be cheaper under provisioned capacity with auto-scaling than under on-demand.

I generally start new projects on on-demand mode, since it removes a whole category of early-stage guesswork, and I switch to provisioned capacity with auto-scaling once traffic patterns become predictable and cost optimization starts to matter.

Data Modeling Fundamentals

Like Cassandra, DynamoDB rewards a query-first modeling approach. Before you design a table, you should have a clear list of every access pattern your application needs to support. This often leads to a technique called single-table design, where instead of creating separate tables for each entity type (users, orders, products), you store multiple entity types in a single DynamoDB table, using carefully constructed partition and sort key values (often prefixed, like USER#123 or ORDER#456) to distinguish between them and enable efficient queries across relationships without needing joins.

Single-table design is powerful but has a real learning curve, and I want to be upfront: many teams, especially early on, are better served by simpler, more intuitive per-entity tables until the access patterns and scale genuinely justify the complexity of a single-table approach.

Practical Example

Imagine an application that needs to support:

  1. Look up a user by user ID
  2. Get all orders for a user, most recent first
  3. Look up an order by order ID directly

A reasonable table design:

  • Base table: partition key PK, sort key SK
    • User item: PK = USER#123, SK = PROFILE
    • Order item: PK = USER#123, SK = ORDER#2024-01-15#456

Querying with PK = USER#123 and a sort key range gives you the user’s profile and orders together, sorted chronologically by the embedded date in the sort key. For looking up an order directly by its ID without knowing the customer, you’d add a GSI with order_id as its partition key.

Consistency Models

DynamoDB offers both eventually consistent reads (the default, and cheaper) and strongly consistent reads (which cost more RCUs but guarantee you see the most recent write). Most read-heavy, tolerant workloads — like displaying a product catalog — work fine with eventual consistency, which is typically consistent within a fraction of a second. For scenarios requiring absolute correctness on every read, like checking an account balance right after an update, strongly consistent reads are worth the extra cost.

Advantages and Limitations

DynamoDB’s biggest advantage is that it’s genuinely serverless — there’s no cluster to manage, patch, or scale manually, and it integrates tightly with the rest of the AWS ecosystem (Lambda, Streams for change data capture, IAM for fine-grained access control). Performance stays consistent regardless of table size, which is a real engineering achievement.

The limitations mostly stem from its rigid query model. There’s no ad-hoc querying, no JOINs, and no easy way to run analytical queries across your whole dataset without exporting to a tool like Athena or Redshift. Query flexibility is deliberately traded for guaranteed performance, and if your application’s access patterns are genuinely unpredictable or exploratory, DynamoDB will fight you every step of the way.

Security Considerations

DynamoDB integrates with AWS IAM for access control, letting you define fine-grained policies — down to which specific attributes or partition key values a given IAM role can access, using condition expressions. Encryption at rest is enabled by default using AWS-managed keys, with the option to use customer-managed KMS keys for additional control. VPC endpoints let you keep DynamoDB traffic off the public internet entirely when accessed from within a VPC.

Comparing DynamoDB to Other NoSQL Databases

DynamoDB and Cassandra share deep architectural DNA, and much of the “model around your access patterns” philosophy is directly transferable between them. The key practical difference is operational: Cassandra requires you to manage your own cluster (or use a managed Cassandra service), while DynamoDB is fully serverless and AWS-managed. Compared to MongoDB, DynamoDB offers a much narrower but more predictable query model, while MongoDB’s document model and richer query language offer more flexibility at some cost to that predictability at extreme scale.

Best Practices

  • Define your access patterns exhaustively before designing your table schema.
  • Prefer composite primary keys when you need to group and range-query related items.
  • Use GSIs for genuinely different access patterns; use LSIs sparingly, and only when created at table setup.
  • Start with on-demand capacity for new or unpredictable workloads; move to provisioned with auto-scaling once patterns stabilize.
  • Consider single-table design only once your access patterns and scale genuinely justify the added complexity.
  • Use IAM condition expressions for fine-grained, attribute-level access control where needed.

DynamoDB Streams and Change Data Capture

One feature I rely on constantly that deserves more attention is DynamoDB Streams, which captures a time-ordered log of item-level modifications (inserts, updates, and deletes) on a table. Each stream record includes the item’s data before and/or after the change, depending on configuration, and these records are available for consumption for 24 hours.

I’ve used DynamoDB Streams as the backbone for a number of event-driven patterns: triggering a Lambda function whenever a new order is written, to kick off downstream processing like sending a confirmation email or updating an inventory count; replicating data into a search index (like OpenSearch) so I can support flexible, full-text queries that DynamoDB itself can’t handle natively; and feeding a data warehouse or analytics pipeline with a continuous stream of changes rather than relying on periodic batch exports. This turns DynamoDB from a purely request-response database into a genuine source of events for the rest of a distributed system, without needing to bolt on a separate change-data-capture tool.

Transactions in DynamoDB

DynamoDB supports ACID transactions across up to 100 items (or 4MB of data) spanning one or more tables, using the TransactWriteItems and TransactGetItems APIs. This is a relatively recent capability (added in 2018) and it addresses a real gap — earlier versions of DynamoDB required careful application-level coordination to achieve anything resembling multi-item consistency.

Transactions are genuinely useful for operations like “deduct inventory and create an order record together, atomically, or neither happens at all,” which are common in e-commerce and financial applications. It’s worth knowing that transactional writes consume roughly double the write capacity of an equivalent non-transactional write, since DynamoDB does additional coordination work to guarantee atomicity — a cost that’s usually well worth paying for operations where partial completion would leave your data in a genuinely inconsistent state, but not something I reach for by default on every write.

Item Size Limits and Large Attribute Handling

DynamoDB caps individual items at 400KB, which is generous for most structured records but does require deliberate handling for anything resembling large binary data, extensive free-text content, or deeply nested structures. My general approach is to store the large content itself in S3, and keep only a reference (the S3 object key) as a DynamoDB attribute. This keeps DynamoDB items small and fast to read and write, while still letting you associate arbitrarily large content with a given item through the reference.

This pattern also has cost implications, since DynamoDB’s pricing is partly driven by item size (larger items consume proportionally more read/write capacity), so keeping items lean isn’t just a hard technical limit to work around — it’s a genuine cost optimization in its own right for any table storing meaningfully large per-item content.

DynamoDB Accelerator (DAX)

For read-heavy workloads where even single-digit millisecond DynamoDB latency isn’t fast enough, DynamoDB Accelerator (DAX) provides an in-memory caching layer that sits directly in front of DynamoDB, offering microsecond-level read latency for cached items while remaining API-compatible with the standard DynamoDB SDK, meaning adopting it usually requires minimal application code changes. I’ve reached for DAX specifically in latency-critical, read-heavy paths — like a product page that gets hit thousands of times per second for a relatively small, hot set of popular items — where shaving even a few milliseconds off read latency has a measurable effect on the overall user experience.

Backup, Restore, and Point-in-Time Recovery

DynamoDB offers on-demand backups that create a full, consistent snapshot of a table without affecting its performance or availability, since the backup process runs independently of the table’s normal read/write capacity. For more granular recovery needs, Point-in-Time Recovery (PITR) continuously backs up table changes, letting you restore a table to any specific second within the preceding 35 days — genuinely useful for recovering from an accidental bulk delete or a bad application deploy that corrupted data, without needing to have manually triggered a backup at exactly the right moment beforehand. I enable PITR by default on any table holding data that would be costly or impossible to reconstruct from another source, given how inexpensive that protection is relative to the risk it mitigates.

Final Thoughts

DynamoDB rewards the same disciplined, query-first thinking that Cassandra does, wrapped in a fully managed, serverless package that removes an enormous amount of operational burden. The learning curve is real, particularly around single-table design, but once you’re comfortable with the model, DynamoDB becomes an incredibly reliable foundation for applications that need predictable performance at any scale without the overhead of managing distributed infrastructure yourself.

Total
0
Shares

Leave a Reply

Previous Post
DynamoDB Data Modeling: Partition Keys, Sort Keys, and Global Secondary Indexes

DynamoDB Data Modeling: Partition Keys, Sort Keys, and Global Secondary Indexes

Next Post
Redis Persistence Options: RDB Snapshots, AOF Logs, and Data Recovery Explained

Redis Persistence Options: RDB Snapshots, AOF Logs, and Data Recovery Explained

Related Posts