MeshWorld India Logo MeshWorld.
aws dynamodb nosql database single-table-design 4 min read

AWS DynamoDB Single-Table Design Cheatsheet: The Complete Reference

Arjun
By Arjun
AWS DynamoDB Single-Table Design Cheatsheet: The Complete Reference

Amazon DynamoDB is a fully managed NoSQL database designed to deliver single-digit millisecond latency at any scale. While traditional relational databases use separate tables for each entity type, high-performance DynamoDB applications utilize Single-Table Design. By placing all entity types in a single table, you can retrieve complex hierarchical structures in a single query request.

This reference sheet covers PK/SK naming conventions, primary access patterns, Global Secondary Indexes (GSIs), transactional operations, and modeling rules.


- **Single-Table Concept**: Place multiple distinct entities (Users, Orders, Items) into a single physical table, using generic attributes like `PK` and `SK`. - **Pre-Joined Querying**: Structure Partition and Sort Keys so that a single `Query` call extracts parent records and associated child rows together. - **GSIs**: Enable auxiliary access patterns by creating secondary index projections using inverted keys (`GSI1PK` and `GSI1SK`). - **Transactions**: Execute atomic all-or-nothing operations across multiple items using `TransactWriteItems`.

Before diving into this cheatsheet, check out my previous deep-dive on AWS IAM Policies & Boundaries Cheatsheet: The Complete Reference to see how we structured these patterns in practice.

Designing Keys & Naming Conventions

Single-table design relies on generic key names (such as PK and SK) because different item types share these same attributes.

Table Structure:
--------------------------------------------------------------------------------------
| PK (String)          | SK (String)          | Type (String) | Attribute Data       |
--------------------------------------------------------------------------------------
| USER#usr_101         | METADATA             | User          | name="Arjun", ...    |
| USER#usr_101         | ORDER#ord_5001       | Order         | total=150.00, ...    |
| USER#usr_101         | ORDER#ord_5002       | Order         | total=45.00, ...     |
| PRODUCT#prod_800     | METADATA             | Product       | price=99.99, ...     |
--------------------------------------------------------------------------------------

Primary Query Access Patterns

Using the key setup above, you can fetch diverse datasets in a single round-trip.

1. Fetch a User Profile

import boto3
from boto3.dynamodb.conditions import Key

dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('meshworld-production-ecom')

# Retrieve only the metadata details of a specific user
response = table.query(
    KeyConditionExpression=Key('PK').eq('USER#usr_101') & Key('SK').eq('METADATA')
)
user_profile = response.get('Items')

2. Fetch a User and All of Their Orders

This pattern is known as a Pre-Joined Query. It returns one User metadata item and multiple Order items together.

# Retrieve the user profile along with all associated order history
response = table.query(
    KeyConditionExpression=Key('PK').eq('USER#usr_101') & Key('SK').begins_with('ORDER#')
)
user_and_orders = response.get('Items')

Using Global Secondary Indexes (GSIs)

Global Secondary Indexes allow you to query your data using alternative partition keys and sort keys.

1. Inverting Keys (GSI1)

To find a user profile by email (where email is normally inside the METADATA attributes), project that attribute into GSI1PK and GSI1SK.

Index Mapping:
--------------------------------------------------------------------------------------
| Item Type | PK              | SK       | GSI1PK (Email)      | GSI1SK (Type)       |
--------------------------------------------------------------------------------------
| User      | USER#usr_101    | METADATA | EMAIL#arjun@mesh.in | USER                |
--------------------------------------------------------------------------------------

2. Querying the GSI

# Query the GSI to find a user by their email address
response = table.query(
    IndexName='GSI1',
    KeyConditionExpression=Key('GSI1PK').eq('EMAIL#arjun@mesh.in') & Key('GSI1SK').eq('USER')
)
user_by_email = response.get('Items')

Implementing ACID Transactions

DynamoDB supports transactional operations that allow you to modify or read up to 100 items atomically. This is critical for operations like order checkout where balance deductions and inventory updates must succeed or fail together.

client = boto3.client('dynamodb')

# Execute atomic updates across multiple keys
try:
    response = client.transact_write_items(
        TransactItems=[
            # 1. Update stock levels on target product
            {
                'Update': {
                    'TableName': 'meshworld-production-ecom',
                    'Key': {
                        'PK': {'S': 'PRODUCT#prod_800'},
                        'SK': {'S': 'METADATA'}
                    },
                    'UpdateExpression': 'SET stock = stock - :qty',
                    'ConditionExpression': 'stock >= :qty',
                    'ExpressionAttributeValues': {
                        ':qty': {'N': '1'}
                    }
                }
            },
            # 2. Record the new order receipt
            {
                'Put': {
                    'TableName': 'meshworld-production-ecom',
                    'Item': {
                        'PK': {'S': 'USER#usr_101'},
                        'SK': {'S': 'ORDER#ord_5003'},
                        'Type': {'S': 'Order'},
                        'total': {'N': '99.99'}
                    }
                }
            }
        ]
    )
    print("Transaction completed successfully!")
except client.exceptions.TransactionCanceledException as e:
    print("Transaction cancelled due to failed check conditions:", e)

Single-Table Design Best Practices

To build scalable DynamoDB single-table models, follow these rules:

  1. Avoid Hot Partitions: Do not place high-write items under the same Partition Key. Spread operations using write-sharding (e.g. appending a random suffix #0 to #9 to partition keys) to avoid throughput bottlenecks.
  2. Keep Items under 400KB: The maximum size of a single DynamoDB item (attributes plus key names) is 400KB. Keep large binary datasets or long logs in Amazon S3 and store the URL reference inside DynamoDB.
  3. Use Sparse Indexes: If a GSI key is not present on an item, DynamoDB does not write that item to the index. You can leverage this to create a “Sparse Index” that only tracks a small subset of your items, keeping storage costs low.
---