DynamoDB Design

Earn 25 points (50 with Pro) in two steps

  1. ① Read through the lesson — each section gets a ✓ as you scroll through it.
  2. ② When every section has a ✓, tap Complete lesson.

0 of 12 read · keep scrolling

✦ See fewer ads and earn double points — 50 a lesson instead of 25 — with Pro

Mastering DynamoDB: Designing High-Performing Architectures

Introduction: Why DynamoDB Design Matters

When building modern, distributed applications, the database is frequently the bottleneck that dictates the performance, scalability, and cost of your entire system. Traditional relational databases (RDBMS) rely on complex joins, normalization, and rigid schemas that work well for specific reporting tasks but often struggle when faced with massive, unpredictable traffic spikes or the need for single-digit millisecond latency at scale. Amazon DynamoDB represents a fundamental shift in how we approach data storage. It is a fully managed, serverless, NoSQL database that provides consistent performance at any scale.

However, DynamoDB is not a drop-in replacement for a relational database. If you try to model a DynamoDB table like a normalized SQL schema, you will likely encounter performance degradation, high costs, and operational headaches. Designing for DynamoDB requires a "query-first" mindset. Instead of thinking about how your data is structured (normalization), you must think about how your application will retrieve that data (access patterns). This lesson will guide you through the principles of designing high-performing DynamoDB architectures, focusing on data modeling, partition keys, indexing, and cost optimization.


Not read yet

The Core Philosophy: Query-First Design

In a relational database, you typically start by identifying your entities and their relationships. You create tables for "Users," "Orders," and "Products," normalizing the data to remove redundancy. When you need to display an order summary, you write a SQL query that joins these three tables together. This is a powerful approach for analytical and ad-hoc reporting, but it is expensive in terms of CPU and memory usage as the dataset grows.

DynamoDB flips this model. Because joins are not supported, you must structure your table so that the data required for a specific user action is physically located together. This often means denormalizing your data—storing information redundantly to ensure that a single request can fetch all the necessary attributes. If you know that your application needs to display a user's profile and their most recent five orders, your data model should allow you to fetch that information in a single round-trip to the database.

Understanding Access Patterns

Before you write a single line of code or create a table, you must document every query your application will perform. This includes listing the entities, the relationships between them, and the specific fields needed for each view or service operation. If you discover a new access pattern later in the development cycle, you might need to add a Global Secondary Index (GSI) or even re-architect your data model.

Callout: Relational vs. NoSQL Modeling Relational modeling focuses on the integrity of the data (normalization) and assumes the database engine will figure out the best way to join tables at query time. NoSQL modeling, specifically for DynamoDB, focuses on the efficiency of the retrieval. You sacrifice storage space (by duplicating data) to gain predictable, high-speed performance that does not degrade as your table size grows to terabytes.


Not read yet

Anatomy of a DynamoDB Table: Keys and Attributes

At the heart of every DynamoDB table are the Primary Key and the attributes. The Primary Key is the only mandatory element, and it is what determines how your data is partitioned across the distributed backend storage.

Partition Keys and Sort Keys

A DynamoDB table can have two types of Primary Keys:

  1. Partition Key (Simple Primary Key): This is a single attribute that determines the partition where the item is stored. DynamoDB uses an internal hash function to decide which physical server handles that specific value.
  2. Partition Key + Sort Key (Composite Primary Key): This allows for more complex data retrieval. The partition key determines the location, while the sort key allows you to store multiple items under the same partition key and sort them. This is essential for one-to-many relationships, such as a user having multiple orders.

Tip: Choosing a High-Cardinality Partition Key Always choose a partition key that has high cardinality, meaning it has a large number of unique values. If you use a partition key like "Status" (which might only be 'Active' or 'Inactive'), all your data will end up on the same physical server, causing a "hot partition." This limits your throughput and prevents the database from scaling.


Not read yet

Advanced Data Modeling Techniques

Denormalization and One-to-Many Relationships

Let’s look at a practical example. Suppose you are building an e-commerce platform. You have a Users entity and an Orders entity. In SQL, you would have two tables. In DynamoDB, you might store both in a single table.

PK (Partition Key) SK (Sort Key) Attributes
USER#123 METADATA {name: "John Doe", email: "[email protected]"}
USER#123 ORDER#2023-01 {total: 50.00, items: [...]}
USER#123 ORDER#2023-02 {total: 75.00, items: [...]}

By using a prefixing strategy (like USER# or ORDER#), you can store different types of entities in the same table. To get a user's profile, you query for PK=USER#123 and SK=METADATA. To get all orders for that user, you query for PK=USER#123 and SK begins_with ORDER#. This is efficient, low-latency, and keeps related data together.

The Adjacency List Pattern

The Adjacency List pattern is a standard way to model complex graphs in DynamoDB. It allows you to represent parent-child relationships effectively. If a Project has many Tasks, you can model it like this:

  • Project Item: PK=PROJECT#A, SK=METADATA
  • Task Item: PK=PROJECT#A, SK=TASK#001

When you want to fetch the project details and all associated tasks, you perform a Query operation on PK=PROJECT#A. Because all these items share the same partition key, they are returned in a single result set.


Not read yet

Working with Global Secondary Indexes (GSIs)

Sometimes, your initial design cannot satisfy all your access patterns. For example, if your primary access pattern is "Find orders by UserID," but you also need to "Find orders by Status," you cannot satisfy both with the same partition key. This is where Global Secondary Indexes (GSIs) come into play.

A GSI is essentially a separate view of your table that uses a different partition key and sort key. When you update an item in your base table, DynamoDB automatically replicates the change to the GSI.

Best Practices for GSIs:

  • Keep them lean: Do not project all attributes into a GSI unless absolutely necessary. Project only the attributes you need for the specific query to save on storage and throughput costs.
  • Be aware of backpressure: If your application writes to the base table faster than the GSI can process the updates, you will experience throttled writes.
  • Monitor GSI health: Always keep an eye on the GSI's write capacity, as it is decoupled from the base table.

Warning: GSI Write Costs Writing to a table with multiple GSIs is more expensive than writing to a table with none. Every time you write to the base table, you are effectively paying for an additional write to each GSI. Design your indexes carefully to avoid unnecessary financial overhead.


Not read yet

Code Implementation: The DynamoDB SDK

Using the AWS SDK for JavaScript (v3), interacting with DynamoDB is straightforward. Below is an example of how to perform a query using the pattern discussed above.

import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { DynamoDBDocumentClient, QueryCommand } from "@aws-sdk/lib-dynamodb";

const client = new DynamoDBClient({});
const docClient = DynamoDBDocumentClient.from(client);

async function getUserOrders(userId) {
  const command = new QueryCommand({
    TableName: "EcommerceTable",
    KeyConditionExpression: "PK = :pk AND begins_with(SK, :sk)",
    ExpressionAttributeValues: {
      ":pk": `USER#${userId}`,
      ":sk": "ORDER#"
    }
  });

  try {
    const response = await docClient.send(command);
    return response.Items;
  } catch (error) {
    console.error("Error fetching orders:", error);
    throw error;
  }
}

Explanation of the Code

  1. DocumentClient: We use the DynamoDBDocumentClient to simplify the process of marshalling and unmarshalling JavaScript objects to DynamoDB JSON format.
  2. KeyConditionExpression: This is the core of the query. We specify that the PK must match our userId and the SK must start with ORDER#. This filters the data at the partition level before it even reaches your application, ensuring high performance.
  3. ExpressionAttributeValues: We use placeholders (:pk, :sk) to prevent injection and to handle reserved words or special characters in our keys.

Not read yet

Performance and Scalability: Avoiding Hot Partitions

A hot partition occurs when one partition in your table receives a significantly higher volume of traffic than others. Because DynamoDB distributes data based on the partition key, if you choose a key that isn't unique enough (like a timestamp or a generic status), all your traffic will hit the same physical machine.

Strategies to Prevent Hot Partitions:

  • Random Suffixing: If you have a highly active item, append a random number to the partition key to spread the load across multiple physical partitions.
  • Sharding: If you know a specific key will be hit frequently, split the data into multiple buckets (e.g., USER#123_1, USER#123_2) and distribute your reads/writes across them.
  • Caching: Use a caching layer like DAX (DynamoDB Accelerator) for read-heavy workloads where the same keys are accessed repeatedly. DAX sits in front of the table and caches the results of GetItem and Query operations.

Callout: When to use DAX DAX is a managed write-through cache. It is excellent for read-heavy applications where you need microsecond latency. However, it adds complexity and cost. Do not use DAX if your application is write-heavy or if your access patterns are already efficient enough with standard DynamoDB queries.


Not read yet

Common Pitfalls to Avoid

1. Designing for "What if"

Many developers try to build a "flexible" schema that can handle future, unknown query requirements. This usually leads to a messy data model that performs poorly. Instead, design specifically for the requirements you have today. DynamoDB is flexible; you can always add a GSI later if you realize you need a new access pattern.

2. Using Scan Operations

The Scan operation reads every single item in your table. It is the most expensive and slowest operation possible in DynamoDB. Only use Scan for administrative tasks, such as exporting data or performing one-time maintenance. If your application logic relies on Scan, your design is likely flawed.

3. Ignoring the 1MB Limit

A single Query or Scan request can only retrieve up to 1MB of data. If your result set is larger, you must use pagination (the LastEvaluatedKey). Failing to implement pagination in your code will lead to incomplete data retrieval and potential application crashes when your dataset grows.

4. Over-indexing

While GSIs are powerful, adding too many of them will bloat your storage and significantly increase your write costs. Only add an index if it is absolutely required to satisfy a core application access pattern.


Not read yet

Step-by-Step: Designing Your First Table

  1. List Access Patterns: Start by writing down every way your application will interact with the data. (e.g., "Get user by email," "Get all orders for a user," "Get order by order ID.")
  2. Define Entities: Map your data into logical entities. Identify which entities have one-to-many or many-to-many relationships.
  3. Create the Schema: Assign a PK and SK for each access pattern. Use prefixes like USER#, ORDER#, PRODUCT# to keep things organized.
  4. Identify GSIs: Look at your access patterns. Any pattern that cannot be satisfied by the primary key needs a GSI.
  5. Review Throughput: Determine if your traffic is read-heavy or write-heavy and choose between On-Demand or Provisioned capacity modes.
  6. Prototype and Test: Use the AWS CLI or local DynamoDB emulator to test your queries against a small dataset. Ensure your KeyConditionExpressions return exactly what you expect.

Not read yet

Comparison Table: Capacity Modes

Feature On-Demand Provisioned
Pricing Pay per request Pay per read/write unit
Scaling Instant, automatic Scheduled or auto-scaling
Best For Unpredictable, sporadic traffic Stable, predictable, high-volume traffic
Management Zero manual intervention Requires monitoring and tuning

Industry Best Practices

Use Attribute Names Wisely

DynamoDB attributes are stored with the data. If you use long, descriptive names for your attributes, you are consuming more storage and increasing the cost of every read and write. Use short attribute names (e.g., n for name, e for email) and keep a mapping document for your team.

Implement Proper Error Handling

DynamoDB can return ProvisionedThroughputExceededException if you exceed your limits. Your application must implement exponential backoff and retry logic. Modern AWS SDKs handle this automatically, but you should configure the retry settings to match your application's tolerance for latency.

Secure Your Data

Use IAM roles to restrict access to your DynamoDB tables. Never embed root credentials in your code. Use Fine-Grained Access Control (FGAC) to limit which users or services can access specific items or attributes within a table.


Not read yet

FAQ: Common Questions

Q: Can I join tables in DynamoDB? A: No. DynamoDB does not support joins. You must model your data so that all required information is retrieved in a single operation.

Q: How do I handle many-to-many relationships? A: You can use a "link" item. For example, to link a User to a Group, you create an item with PK=USER#123 and SK=GROUP#456. You can also create a GSI with the PK as the Group and the SK as the User to query the relationship in reverse.

Q: Is DynamoDB truly serverless? A: Yes. You do not manage any servers, patches, or backups. You simply define your table, and AWS handles the underlying infrastructure and scaling.

Q: What happens if I make a mistake in my primary key design? A: Unlike a SQL database where you can easily add a column, changing a primary key in DynamoDB requires migrating data to a new table. This is why thorough planning during the design phase is critical.


Not read yet

Key Takeaways

  1. Think Query-First: Never design your table structure until you have defined exactly how your application will query the data.
  2. Denormalize for Speed: Embrace data duplication. Storing related entities together in a single item or partition is the key to high performance.
  3. Master the Keys: The Partition Key and Sort Key are your most important tools. Use them to organize your data hierarchically and enable efficient range queries.
  4. Use GSIs Sparingly: Global Secondary Indexes are powerful but come with a cost. Only create them when necessary to satisfy specific, high-value access patterns.
  5. Avoid Scans: The Scan operation is a performance killer. If your code uses it for anything other than administrative tasks, rethink your data model.
  6. Monitor and Optimize: Keep an eye on your partition metrics. If you see signs of a "hot partition," act early to redistribute your load using sharding or random suffixing.
  7. Choose the Right Capacity: Start with On-Demand capacity for new projects. Switch to Provisioned capacity only when your traffic patterns become predictable and you need to optimize costs at scale.

By following these principles, you will move beyond treating DynamoDB as a simple key-value store and start using it as a highly efficient, scalable engine that powers world-class applications. Remember that the goal is not to force your data into a specific shape, but to ensure that your database perfectly supports the way your users interact with your software.

Not read yet

Each section gets a ✓ as you scroll through it. Tap the button to jump to the next one.