NoSQL Databases

DynamoDB Single-Table Design: Access Patterns, GSIs, and Capacity Planning

Design DynamoDB access patterns with composite keys, sparse GSIs, paginated queries, transactions, and evidence-based capacity planning.

JusDB Team
Published June 13, 2025
Updated August 1, 2026
6 min read

DynamoDB single-table design stores multiple entity types in one table so a request can retrieve a related item collection through known partition and sort keys. It can reduce network round trips for a well-understood workload, but it is not a rule for every system. Begin with access patterns, consistency needs, item sizes, write rates, and ownership boundaries; choose the table count after those facts are explicit.

In short
  • Write each access pattern before inventing key names.
  • Use composite sort keys to group related items and retrieve ranges without scans.
  • Add a GSI only for a real alternate lookup; sparse indexes can limit which items are copied.
  • Query responses are paginated at 1 MB, filters run after items are read, and GSI reads are eventually consistent.
  • Measure hot keys with throttling reason metrics and Contributor Insights rather than assuming table-wide capacity is the only constraint.

Start with an access-pattern table

For an order system, list the exact operations and expected scale:

OperationKey conditionConsistency
Get customer profilePK=CUSTOMER#id, SK=PROFILEStrong if read from the base table
List a customer's orders newest firstPK=CUSTOMER#id, begins_with(SK, ORDER#)Base-table choice
Get one order and its linesPK=ORDER#id with an order/line sort-key rangeBase-table choice
List pending orders by monthSparse GSI keyed by status bucket and timeEventually consistent

The partition key is hashed to select an internal partition. Items with the same partition-key value remain an item collection ordered by sort key. A composite sort key such as ORDER#2026-08-01T12:34:56Z#01J... supports prefix and range queries while preserving a deterministic tie-breaker.

A compact item model

javascript
// Customer profile
{
  PK: "CUSTOMER#c-42",
  SK: "PROFILE",
  entityType: "Customer",
  name: "Asha"
}

// Order summary in the customer's collection
{
  PK: "CUSTOMER#c-42",
  SK: "ORDER#2026-08-01T12:34:56Z#o-900",
  entityType: "OrderSummary",
  orderId: "o-900",
  status: "PENDING",
  GSI1PK: "STATUS#PENDING#2026-08",
  GSI1SK: "2026-08-01T12:34:56Z#o-900"
}

// Order aggregate
{
  PK: "ORDER#o-900",
  SK: "ORDER",
  entityType: "Order",
  customerId: "c-42",
  status: "PENDING"
}

Only items carrying both GSI key attributes appear in that index. That makes the status index sparse: profiles and unrelated entities do not create index entries. Keep key grammar documented and validate it in one data-access layer; ambiguous delimiters and ad hoc key construction are difficult to migrate.

Query correctly: key conditions first, then pagination

A DynamoDB Query requires a partition-key equality condition and can add a sort-key condition. A FilterExpression is applied after DynamoDB reads the page, so it does not reduce the read capacity consumed for those evaluated items. Redesign a frequently filtered access pattern as a key condition or index rather than hiding it behind a filter.

Each Query response contains at most 1 MB before filtering. Continue while LastEvaluatedKey is present:

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

const ddb = DynamoDBDocumentClient.from(new DynamoDBClient({}))
let ExclusiveStartKey
const items = []

do {
  const page = await ddb.send(new QueryCommand({
    TableName: "Commerce",
    KeyConditionExpression: "PK = :pk AND begins_with(SK, :prefix)",
    ExpressionAttributeValues: {
      ":pk": "CUSTOMER#c-42",
      ":prefix": "ORDER#2026-08"
    },
    ScanIndexForward: false,
    ExclusiveStartKey
  }))
  items.push(...(page.Items ?? []))
  ExclusiveStartKey = page.LastEvaluatedKey
} while (ExclusiveStartKey)

Set a deliberate page or item limit for interactive APIs; do not accumulate an unbounded result in memory as the example does. GSI queries cannot request strongly consistent reads. If a workflow needs read-after-write behavior, read the authoritative base-table item or use a different coordination design.

Design GSI cost from index entries

A GSI has its own key schema, projections, storage, and throughput behavior. A base-table write consumes index write capacity only when it creates, changes, or removes that index's entry. The units depend on the index entry size, not a universal multiplier. An ALL projection can enlarge every matching index entry; INCLUDE or KEYS_ONLY can be cheaper, but may require a second base-table read.

DynamoDB's default quota is 20 GSIs per table. Treat that as a quota, not a target. Every index adds write paths, failure signals, backfill considerations, and operational cost. Overloading an index with multiple entity types is valid when each type has an unambiguous key grammar and compatible capacity distribution.

Atomic multi-item changes

TransactWriteItems supports up to 100 actions totaling no more than 4 MB in one account and Region. Two actions cannot target the same item. Use condition expressions to protect invariants and a ClientRequestToken for idempotency when retrying the same request.

javascript
import { TransactWriteCommand } from "@aws-sdk/lib-dynamodb"

await ddb.send(new TransactWriteCommand({
  ClientRequestToken: requestId,
  TransactItems: [
    {
      Update: {
        TableName: "Commerce",
        Key: { PK: "ORDER#o-900", SK: "ORDER" },
        UpdateExpression: "SET #s = :paid",
        ConditionExpression: "#s = :pending",
        ExpressionAttributeNames: { "#s": "status" },
        ExpressionAttributeValues: { ":paid": "PAID", ":pending": "PENDING" }
      }
    },
    {
      Put: {
        TableName: "Commerce",
        Item: { PK: "ORDER#o-900", SK: "PAYMENT#p-12", amount: 4200 },
        ConditionExpression: "attribute_not_exists(PK)"
      }
    }
  ]
}))

Transactional writes consume two write request units for each 1 KB item involved, subject to normal item-size rounding. Calculate cost from every affected item and index entry instead of labeling the whole operation simply “2x.”

Capacity mode and hot keys

On-demand mode removes manual throughput provisioning, but it does not make every instantaneous jump unthrottled. A table can immediately sustain its previous peak and up to twice that peak; a jump beyond twice the previous peak within roughly 30 minutes can throttle. Pre-warm in steps when a launch is expected to exceed that envelope. Provisioned mode with auto scaling can be appropriate for predictable workloads and explicit cost controls.

A concentrated partition-key value can still reach a key-range throughput limit while aggregate table capacity remains available. Look at ThrottlingReason, key-range throttle metrics, application retry telemetry, and DynamoDB Contributor Insights for frequently accessed keys. Shard a key only after evidence identifies a hot access pattern, because write sharding makes reads and ordering more complex.

Monitoring example

bash
aws cloudwatch get-metric-statistics --namespace AWS/DynamoDB --metric-name ConsumedReadCapacityUnits --dimensions Name=TableName,Value=Commerce --statistics Sum --period 60 --start-time 2026-08-01T00:00:00Z --end-time 2026-08-01T01:00:00Z

Add the GlobalSecondaryIndexName dimension to inspect one GSI. Capacity metrics are table/index level; use Contributor Insights when the question is which keys dominate traffic. Also monitor successful-request latency, system and user errors, conditional-check failures, transaction conflicts, and application retry exhaustion.

Official primary sources

Working with JusDB on DynamoDB design

JusDB reviews access patterns, key distribution, index projections, capacity evidence, and migration plans before a key schema becomes expensive to change.

Explore JusDB DynamoDB services →  |  Talk to a database engineer

Share this article

JusDB Team

Official JusDB content team

Keep reading

High Performance with MongoDB: A Top-Down Tuning Guide

A top-down playbook for high-performance MongoDB: measure with the profiler and explain(), model for access patterns, index by the ESR rule, keep the working set in the WiredTiger cache, pool connections, and scale reads with secondaries and sharding — with flow diagrams for each layer.

MongoDB14 minJun 6, 2026
Read

MongoDB Explained (2026): Replica Sets, Sharding, Atlas & Production Patterns

Complete MongoDB guide covering document modeling, aggregation pipelines, sharding, replication, and Atlas deployment. Learn when MongoDB is the right choice for your application.

MongoDB5 minMay 13, 2026
Read

ScyllaDB vs Apache Cassandra: Performance and Operational Differences

Compare ScyllaDB and Apache Cassandra — throughput, latency, operational complexity, and when to switch

Cassandra11 minJan 22, 2026
Read