AWS / Cloud

AWS DynamoDB Single-Table Design, GSI Patterns & Production Best Practices 2026

DynamoDB can handle millions of requests per second at single-digit millisecond latency — but only if you design your data model correctly from day one. Engineers who bring a relational mindset to DynamoDB end up with expensive hot partitions, missing access patterns, and runaway costs. This comprehensive guide covers AWS DynamoDB single-table design, GSI patterns, partition key strategies, capacity planning, and every production best practice you need to build bulletproof NoSQL data models on AWS in 2026.

Md Sanwar Hossain April 10, 2026 22 min read AWS / Cloud
AWS DynamoDB single-table design and production best practices for high-scale NoSQL applications

TL;DR — Key DynamoDB Design Rule

"Define your access patterns before you design your table. DynamoDB rewards you for knowing exactly how your application queries data, and punishes you for discovering new patterns after launch. Single-table design, entity prefixes, and GSI overloading are the tools — access patterns first is the philosophy."

Table of Contents

  1. Why DynamoDB? When to Choose NoSQL Over RDS
  2. DynamoDB Core Concepts: Tables, Items, Keys
  3. Single-Table Design: The Complete Guide
  4. Partition Key Strategies: Avoiding Hot Partitions
  5. GSI & LSI Patterns: Inverting Access Patterns
  6. Capacity Planning: On-Demand vs Provisioned Throughput
  7. DynamoDB Streams & Change Data Capture
  8. Transactions & Conditional Writes
  9. DynamoDB Production Best Practices
  10. Security: Encryption, IAM & VPC Endpoints
  11. Monitoring, Performance Tuning & Anti-Patterns
  12. Conclusion & Production Checklist

1. Why DynamoDB? When to Choose NoSQL Over RDS

The single biggest mistake engineers make is choosing DynamoDB because it's serverless, cheap, or trendy — without asking whether their access patterns actually benefit from its architecture. DynamoDB is a key-value and document store optimized for predictable, high-throughput workloads at any scale. It is not a replacement for PostgreSQL in every scenario.

The core trade-off: DynamoDB sacrifices query flexibility (no arbitrary SQL joins, no ad-hoc aggregations) in exchange for guaranteed single-digit millisecond latency at any scale, automatic sharding, built-in replication across three AZs, and zero server management. When your access patterns are well-known and your scale demands it, DynamoDB wins decisively.

Factor DynamoDB RDS / PostgreSQL
Access Patterns Known, predictable, high volume Flexible, ad-hoc, exploratory
Scalability Unlimited, automatic horizontal scaling Vertical + read replicas, manual sharding
Consistency Model Eventually consistent (default), strongly consistent available ACID transactions by default
Joins & Aggregations Not supported — model pre-joins Full SQL: JOINs, GROUP BY, window functions
Latency at Scale <5ms p99 at millions of TPS Increases under heavy load without tuning
Ideal Use Cases User sessions, shopping carts, leaderboards, IoT, gaming Financial ledgers, reporting, complex business logic

Strong DynamoDB Indicators

  • Your application serves >10,000 requests/second and latency SLAs are strict (<10ms)
  • Data access patterns are well-understood upfront (e-commerce cart, user profiles, session state)
  • You need zero ops — no patching, no vacuuming, no replication configuration
  • Workloads are bursty and unpredictable — on-demand capacity mode handles traffic spikes automatically
  • You're building event-driven systems and need DynamoDB Streams for CDC fanout

2. DynamoDB Core Concepts: Tables, Items, Attributes, Primary Keys

Before diving into single-table design, you must have a precise mental model of DynamoDB's data structures. Unlike relational databases where you think in terms of rows, columns, and foreign keys, DynamoDB thinks in terms of items and attributes addressed by keys.

Primary Key Types

Partition keys are hashed internally — you cannot range-query on a partition key. Sort keys are stored in sorted order within a partition, enabling range queries, begins_with, and between conditions. This asymmetry is the foundation of DynamoDB's entire access pattern model.

Item Size, Attribute Types, and Limits

3. Single-Table Design: The Complete Guide

Single-table design (STD) is the approach of storing all entity types for an application in a single DynamoDB table, using generic attribute names (PK, SK, GSI1PK, GSI1SK) with structured prefixes to differentiate entity types. It is the recommended approach by AWS DynamoDB experts including Rick Houlihan, who pioneered the pattern.

The core insight: DynamoDB charges per request, not per table scan. Fetching a user and all their orders in a single Query call (using a composite key pattern) is far cheaper and faster than making multiple round trips to separate tables. Single-table design co-locates related items on the same partition, enabling single-request retrieval of hierarchical data.

Access Patterns First: The Canonical Workflow

  1. List every access pattern your application needs — get user by ID, list orders for user, get order items, find products by category, etc.
  2. Determine which patterns are primary (used on every request) and which are secondary (used less frequently).
  3. Assign each access pattern to either the base table (PK+SK), a GSI, or an LSI.
  4. Design PK and SK values to satisfy the maximum number of patterns with the minimum number of indexes.
  5. Only then write code. The table design is the contract; the code implements it.

Entity Prefixes & Key Schema

Use string prefixes to encode the entity type directly in the key value. This makes items self-describing and prevents key collisions between entity types:

# E-Commerce Single-Table Key Schema

# Get user by ID
PK = "USER#u-123"          SK = "PROFILE"

# Get all orders for user (Query PK = USER#u-123, SK begins_with ORDER#)
PK = "USER#u-123"          SK = "ORDER#2026-04-10T12:00:00Z#o-789"

# Get specific order
PK = "ORDER#o-789"         SK = "METADATA"

# Get all items in an order
PK = "ORDER#o-789"         SK = "ITEM#p-456"
PK = "ORDER#o-789"         SK = "ITEM#p-789"

# Get product by ID
PK = "PRODUCT#p-456"       SK = "DETAILS"

# GSI1: Get orders by status (GSI1PK = ORDER_STATUS#PENDING)
GSI1PK = "ORDER_STATUS#PENDING"   GSI1SK = "2026-04-10T12:00:00Z#o-789"
Access Pattern PK SK Index
Get user profile USER#<id> PROFILE Base table
List orders for user USER#<id> ORDER# (begins_with) Base table
Get order details ORDER#<id> METADATA Base table
List items in order ORDER#<id> ITEM# (begins_with) Base table
Orders by status + date ORDER_STATUS#<status> <timestamp>#<orderId> GSI1
Products by category CATEGORY#<name> PRODUCT#<id> GSI2
AWS DynamoDB single-table design entity relationship and key schema diagram
AWS DynamoDB Single-Table Design — entity relationships, key schema, and GSI overloading patterns. Source: mdsanwarhossain.me

4. Partition Key Strategies: Avoiding Hot Partitions

A hot partition occurs when too many requests are routed to a single physical partition. DynamoDB distributes data across partitions using a hash of the partition key. If many items share the same partition key — or one partition key attracts disproportionate traffic — that partition's throughput limit (3,000 RCU and 1,000 WCU) becomes the bottleneck for your entire workload.

Write Sharding

For high-cardinality write targets (a global leaderboard counter, a viral post's like count), append a random suffix to spread writes across N logical shards:

// Write sharding: spread writes across 10 shards
int SHARD_COUNT = 10;
String shardSuffix = String.valueOf(ThreadLocalRandom.current().nextInt(SHARD_COUNT));
String pk = "LEADERBOARD#global#" + shardSuffix;

// To read total, query all shards and aggregate in application
for (int i = 0; i < SHARD_COUNT; i++) {
    String shardPk = "LEADERBOARD#global#" + i;
    // GetItem / Query each shard and sum
}

Adaptive Capacity & Burst

DynamoDB's Adaptive Capacity (enabled by default) automatically redistributes throughput from underused partitions to overloaded ones within a table. It can absorb short bursts on hot partitions but is not a substitute for good key design. Additionally, DynamoDB's burst capacity retains unused RCU/WCU for up to 300 seconds and can apply them during spikes. Never rely on burst as a design strategy — use it as a safety net only.

Partition Key Anti-Patterns to Avoid

  • Using a low-cardinality attribute as PK (e.g., status = "ACTIVE" / "INACTIVE") — concentrates all traffic on two partitions
  • Using a sequential ID or timestamp as PK — causes monotonic writes to the same partition ("hot end")
  • Using a single tenant ID as PK in a multi-tenant system when one tenant dominates traffic — classic hot partition problem
  • Using date strings as PK (e.g., "2026-04-10") — all writes for a day hit one partition; use date + random suffix or time-based sharding

5. GSI & LSI Patterns: Inverting Access Patterns

Global Secondary Indexes (GSIs) are the primary mechanism for supporting access patterns that the base table's PK+SK cannot directly serve. A GSI is essentially a second copy of your table (a projection) organized around a different partition key and optional sort key. You can have up to 20 GSIs per table.

GSI vs LSI: When to Use Each

GSI Overloading

GSI overloading reuses the same GSI to serve multiple access patterns by populating the GSI key attributes with different values per entity type. This reduces the total number of indexes needed:

// GSI1 (GSI1PK + GSI1SK) serves multiple patterns via overloading:

// Pattern A: Get all employees in a department
// GSI1PK = "DEPT#engineering"   GSI1SK = "EMP#e-001"

// Pattern B: Get all open tickets by priority  
// GSI1PK = "PRIORITY#HIGH"      GSI1SK = "TICKET#2026-04-10T09:00:00Z"

// Pattern C: Get products by supplier
// GSI1PK = "SUPPLIER#s-42"      GSI1SK = "PRODUCT#p-200"

// All three patterns use GSI1 — different entity types, same index

Sparse Indexes

DynamoDB only adds items to a GSI if the GSI's partition key attribute exists on the item. This creates sparse indexes — indexes that only contain a subset of items. This is powerful for filtering: only write the GSI key attribute on items that should appear in that index. For example, set needsReview = "true" only on flagged records, and create a GSI on needsReview. The GSI will only contain flagged items, giving you a cheap filter with no scan.

6. Capacity Planning: On-Demand vs Provisioned Throughput

Getting capacity mode and sizing right is one of the most impactful cost and reliability decisions you'll make with DynamoDB. The choice between on-demand and provisioned isn't binary — it depends on your traffic predictability and cost sensitivity.

WCU & RCU Formulas

// WCU (Write Capacity Unit) Calculation
// 1 WCU = 1 strongly consistent write of items up to 1 KB/second
// Item size rounds UP to nearest 1 KB

// Example: 500 writes/sec, average item size 2.5 KB
WCUs_required = 500 * ceil(2.5) = 500 * 3 = 1500 WCU

// RCU (Read Capacity Unit) Calculation  
// 1 RCU = 1 strongly consistent read of items up to 4 KB/second
//       = 2 eventually consistent reads of items up to 4 KB/second

// Example: 2000 reads/sec, eventually consistent, average item 3 KB
RCUs_required = ceil(2000 / 2) * ceil(3 / 4) = 1000 * 1 = 1000 RCU

// Transactional writes cost 2x WCU; transactional reads cost 2x RCU

// Monthly cost estimate (us-east-1, 2026 pricing):
// On-Demand:   $1.25 per million write requests, $0.25 per million reads
// Provisioned: $0.00065/WCU-hour, $0.00013/RCU-hour
// 1500 WCU provisioned = 1500 * 0.00065 * 730 hours = ~$712/month
// 1500 WCU on-demand at 100M writes = 100 * $1.25 = $125/month
// Break-even: ~57M writes/month favors on-demand; above that, provisioned wins

On-Demand vs Provisioned Decision Matrix

  • On-Demand: Choose when traffic is unpredictable, spiky, or bursty. New applications, dev/test environments, event-driven workloads with unknown peak patterns. Zero capacity planning required — DynamoDB scales instantly. Pay per request.
  • Provisioned + Auto-Scaling: Choose when traffic is predictable and you need cost optimization. Set min/max capacity and a target utilization (70–80%). Auto-scaling responds in ~5 minutes — combine with DynamoDB burst to handle short spikes.
  • Reserved Capacity: Purchase 100+ WCU/RCU for 1 or 3 years at 53–77% savings over provisioned on-demand pricing. Only buy what you're confident in; combine with auto-scaling for the variable portion.

7. DynamoDB Streams & Change Data Capture

DynamoDB Streams captures a time-ordered sequence of item-level changes (inserts, updates, deletes) in a DynamoDB table. Each stream record contains the old and/or new image of the modified item. Records are retained for 24 hours and can be consumed by AWS Lambda, Kinesis Data Streams (via Kinesis Data Streams for DynamoDB), or custom consumers.

Event-Driven Patterns with Streams

Stream View Types

8. DynamoDB Transactions & Conditional Writes

DynamoDB transactions (launched in 2018) provide ACID guarantees across multiple items within a single AWS account and region. TransactWrite supports up to 100 items across one or more tables in a single atomic operation. TransactGet reads up to 100 items consistently.

Conditional Writes & Optimistic Locking

Conditional expressions make a write (Put, Update, Delete) succeed only if a specified condition is true at write time — without requiring a transaction. This is the cheapest form of optimistic concurrency control in DynamoDB:

// Optimistic locking with version attribute
UpdateItemRequest request = UpdateItemRequest.builder()
    .tableName("AppTable")
    .key(Map.of(
        "PK", AttributeValue.fromS("ORDER#o-789"),
        "SK", AttributeValue.fromS("METADATA")
    ))
    .updateExpression("SET #status = :newStatus, version = :newVersion")
    .conditionExpression("version = :expectedVersion")
    .expressionAttributeNames(Map.of("#status", "status"))
    .expressionAttributeValues(Map.of(
        ":newStatus", AttributeValue.fromS("SHIPPED"),
        ":newVersion", AttributeValue.fromN("3"),
        ":expectedVersion", AttributeValue.fromN("2")  // Must match current
    ))
    .build();
// Throws ConditionalCheckFailedException if version has changed (concurrent update)

Use attribute_not_exists(PK) as a condition on PutItem to implement idempotent creates — the write only succeeds if the item doesn't already exist, preventing duplicate creation under at-least-once delivery.

9. DynamoDB Production Best Practices

Java SDK v2: DynamoDB Enhanced Client

The AWS SDK v2 Enhanced Client provides a type-safe, annotation-driven API for DynamoDB that avoids manual AttributeValue marshalling. It is the recommended approach for Java Spring Boot microservices in 2026:

// DynamoDB Enhanced Client — Save & Get with annotations (Java SDK v2)

@DynamoDbBean
public class Order {
    private String pk;
    private String sk;
    private String orderId;
    private String userId;
    private String status;
    private Instant createdAt;
    private BigDecimal totalAmount;

    @DynamoDbPartitionKey
    @DynamoDbAttribute("PK")
    public String getPk() { return pk; }

    @DynamoDbSortKey
    @DynamoDbAttribute("SK")
    public String getSk() { return sk; }

    // GSI key attribute
    @DynamoDbSecondaryPartitionKey(indexNames = "GSI1")
    @DynamoDbAttribute("GSI1PK")
    public String getGsi1Pk() {
        return "ORDER_STATUS#" + this.status;
    }

    // ... getters/setters omitted for brevity
}

@Service
public class OrderRepository {
    private final DynamoDbTable<Order> table;

    public OrderRepository(DynamoDbEnhancedClient enhancedClient) {
        this.table = enhancedClient.table("AppTable", TableSchema.fromBean(Order.class));
    }

    public void saveOrder(Order order) {
        order.setPk("ORDER#" + order.getOrderId());
        order.setSk("METADATA");
        table.putItem(order);
    }

    public Optional<Order> getOrder(String orderId) {
        Key key = Key.builder()
            .partitionValue("ORDER#" + orderId)
            .sortValue("METADATA")
            .build();
        return Optional.ofNullable(table.getItem(key));
    }
}

GSI Query with Java SDK v2

// Query GSI1: Get pending orders after a specific timestamp

public List<Order> getPendingOrdersAfter(Instant after) {
    DynamoDbIndex<Order> gsi1 = table.index("GSI1");

    QueryConditional queryConditional = QueryConditional
        .sortGreaterThan(Key.builder()
            .partitionValue("ORDER_STATUS#PENDING")
            .sortValue(after.toString())
            .build());

    PageIterable<Order> results = gsi1.query(r -> r
        .queryConditional(queryConditional)
        .limit(100)
        .scanIndexForward(false)  // descending: most recent first
    );

    return results.items().stream().collect(Collectors.toList());
}

TTL, Batch Operations & Error Handling

10. DynamoDB Security: Encryption, IAM Fine-Grained Access, VPC Endpoints

Encryption at Rest

All DynamoDB tables are encrypted at rest by default using AWS-owned keys (free). For compliance requirements (PCI-DSS, HIPAA, SOC2), use AWS KMS Customer Managed Keys (CMK): you control the key lifecycle, can audit all key usage in CloudTrail, and can revoke access by disabling the key. CMK encryption adds ~1ms of latency to operations and costs $1/month per key plus $0.03 per 10,000 API calls to KMS.

IAM Fine-Grained Access Control

DynamoDB supports attribute-level IAM conditions, enabling you to restrict which items or attributes a principal can access based on their identity. This is called Fine-Grained Access Control (FGAC):

// IAM policy: Allow users to access only their own items (leading key condition)
{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Action": ["dynamodb:GetItem", "dynamodb:PutItem", "dynamodb:UpdateItem"],
    "Resource": "arn:aws:dynamodb:us-east-1:123456789:table/AppTable",
    "Condition": {
      "ForAllValues:StringLike": {
        "dynamodb:LeadingKeys": ["USER#${cognito-identity.amazonaws.com:sub}"]
      }
    }
  }]
}

VPC Endpoints

By default, DynamoDB is accessed over the public internet (HTTPS). For applications running in a VPC (EC2, ECS, EKS, Lambda in VPC), use a VPC Gateway Endpoint for DynamoDB. This routes traffic through the AWS private network — no NAT Gateway charges, no public IP required, and traffic never leaves the AWS backbone. VPC Endpoint policies add an additional IAM authorization layer that applies to all traffic through the endpoint. This is a zero-cost security and cost optimization for any production application.

11. Monitoring, Performance Tuning & Anti-Patterns to Avoid

Key CloudWatch Metrics to Monitor

DynamoDB Anti-Patterns: What NOT to Do

Top Anti-Patterns That Kill DynamoDB Performance

  • Scan instead of Query: A full-table Scan reads every item, consuming enormous RCUs and returning slowly. If you're using Scan in production for anything other than a one-time data migration, your data model needs to change. Define GSIs to serve all access patterns with Query operations.
  • N+1 Query Problem: Fetching a list of IDs and then issuing one GetItem per ID is the DynamoDB equivalent of N+1 queries. Use BatchGetItem or redesign the data model to fetch related items with a single Query.
  • Storing large blobs in DynamoDB: DynamoDB items max out at 400 KB and charge per KB. Store large binary objects (images, PDFs, videos) in S3 and keep only the S3 URL reference in DynamoDB.
  • Using a relational schema: Normalizing data into many small tables and then joining at application level is the worst of both worlds — it loses DynamoDB's single-partition co-location benefits and adds code complexity. Design for read patterns, not normalization.
  • Ignoring GSI throttling: GSIs have their own throughput. A GSI write consumes WCU from the GSI's capacity, not the base table. A hot GSI is a common silent killer — always provision GSI capacity separately.
  • Using DynamoDB for reporting: DynamoDB is not an analytics database. For aggregate reports, use DynamoDB Streams → Kinesis Firehose → S3 → Athena or Redshift for reporting queries. Never run report-style scans in production.

DynamoDB Accelerator (DAX)

DynamoDB Accelerator (DAX) is a fully managed, in-memory cache for DynamoDB that delivers microsecond read latency. It is a drop-in replacement for the DynamoDB SDK client (API-compatible). DAX caches GetItem and Query results with a configurable TTL (default 5 minutes). Use DAX when your application has read-heavy workloads with high cache hit rates (profile pages, product catalog, leaderboards). Note that DAX runs inside your VPC on dedicated nodes — it is not serverless and adds $0.269–$3.503/hour per node depending on instance type. For most write-heavy or low-latency-sensitive write applications, DynamoDB's native <5ms latency is sufficient without DAX.

12. Conclusion & Production Checklist

AWS DynamoDB is one of the most powerful databases in the cloud when you design it correctly, and one of the most expensive and frustrating when you don't. The single-table design philosophy, access-patterns-first thinking, and disciplined use of GSI overloading and sparse indexes give you a data model that can serve millions of requests per second at <5ms latency indefinitely. The engineers who master DynamoDB production best practices build systems that never need database downtime, never need to scale up a server, and never pay for capacity they don't use.

In 2026, DynamoDB continues to evolve — zero-ETL integrations with Redshift and S3, improved DynamoDB Streams throughput, and deeper integration with AWS Graviton-powered infrastructure make it a stronger choice than ever for the right workloads. Master the fundamentals in this guide and you'll be equipped to design DynamoDB tables that scale with your business.

DynamoDB Production Launch Checklist

  • Document all access patterns before designing the table schema
  • Use single-table design with entity prefixes (USER#, ORDER#, PRODUCT#)
  • High-cardinality partition key — verify key distribution covers >100 distinct values
  • No Scan in production code — every access pattern served by Query or GetItem
  • GSI capacity provisioned separately from base table
  • On-demand mode for new tables until traffic patterns are established
  • Auto-scaling configured with target 70% utilization on provisioned tables
  • TTL enabled for all time-bound data (sessions, tokens, temp state)
  • Point-in-Time Recovery (PITR) enabled on all production tables
  • VPC Gateway Endpoint configured for private DynamoDB access
  • CloudWatch alarms on ThrottledRequests, ConsumedCapacity, and p99 latency
  • IAM least-privilege with resource-level and condition-based policies
  • BatchWriteItem UnprocessedItems handled with exponential back-off
  • Idempotent writes using attribute_not_exists or version conditions

Leave a Comment

Related Posts

Md Sanwar Hossain - Software Engineer
Md Sanwar Hossain

Software Engineer · Java · Spring Boot · Microservices · AWS · DynamoDB

All Posts
Last updated: April 10, 2026