DEV Community

Said Olano
Said Olano

Posted on

AWS DynamoDB: NoSQL Database Patterns (2026-08-29 01:34)

AWS DynamoDB: NoSQL Database Patterns

Amazon DynamoDB is a fully managed NoSQL database service that delivers single-digit millisecond performance at any scale. However, its performance and cost-effectiveness depend heavily on how well you model your data. Unlike relational databases, DynamoDB rewards developers who design their schema around access patterns rather than entity relationships.

This post covers the foundational concepts and proven patterns for building efficient DynamoDB applications.

Core Concepts

Before diving into patterns, let's establish the building blocks.

Primary Keys

DynamoDB tables require a primary key that uniquely identifies each item. There are two types:

  • Partition key (simple primary key): A single attribute that determines the physical partition where the item is stored.
  • Composite key (partition key + sort key): Enables multiple items to share a partition key, sorted by the sort key.
import boto3

dynamodb = boto3.resource('dynamodb')

table = dynamodb.create_table(
    TableName='Orders',
    KeySchema=[
        {'AttributeName': 'PK', 'KeyType': 'HASH'},   # Partition key
        {'AttributeName': 'SK', 'KeyType': 'RANGE'}   # Sort key
    ],
    AttributeDefinitions=[
        {'AttributeName': 'PK', 'AttributeType': 'S'},
        {'AttributeName': 'SK', 'AttributeType': 'S'}
    ],
    BillingMode='PAY_PER_REQUEST'
)
Enter fullscreen mode Exit fullscreen mode

Think in Access Patterns

The golden rule: design your table based on how you query, not how you store. List every access pattern your application needs before designing the schema.

Pattern 1: Single-Table Design

A common instinct is to create one table per entity, mimicking relational design. In DynamoDB, the recommended approach is often the opposite: store multiple entity types in a single table.

Consider an application with Users and their Orders. Using generic key names (PK, SK), we can co-locate related items:

PK SK Attributes
USER#123 PROFILE name, email
USER#123 ORDER#1001 total, status
USER#123 ORDER#1002 total, status

Fetching a user and all their orders becomes a single query:

response = table.query(
    KeyConditionExpression=Key('PK').eq('USER#123')
)
Enter fullscreen mode Exit fullscreen mode

To fetch just the profile, add a sort key condition:

response = table.query(
    KeyConditionExpression=Key('PK').eq('USER#123') & Key('SK').eq('PROFILE')
)
Enter fullscreen mode Exit fullscreen mode

Pattern 2: Global Secondary Indexes (GSIs)

When you need to query by attributes other than the primary key, use a Global Secondary Index. A GSI has its own partition and sort key and is maintained automatically by DynamoDB.

For example, to query orders by status:

# Add a GSI with StatusKey as partition key
GlobalSecondaryIndexes=[
    {
        'IndexName': 'StatusIndex',
        'KeySchema': [
            {'AttributeName': 'status', 'KeyType': 'HASH'},
            {'AttributeName': 'createdAt', 'KeyType': 'RANGE'}
        ],
        'Projection': {'ProjectionType': 'ALL'}
    }
]
Enter fullscreen mode Exit fullscreen mode

Query pending orders sorted by creation time:

response = table.query(
    IndexName='StatusIndex',
    KeyConditionExpression=Key('status').eq('PENDING')
)
Enter fullscreen mode Exit fullscreen mode

Tip: GSIs are eventually consistent and have their own provisioned throughput. Use sparse indexes (only indexing items that have the GSI key attribute) to reduce cost.

Pattern 3: Composite Sort Keys

Sort keys support range queries using begins_with, between, and comparison operators. By concatenating hierarchical data into the sort key, you enable flexible querying.

# SK format: COUNTRY#USA#STATE#CA#CITY#SF
response = table.query(
    KeyConditionExpression=Key('PK').eq('LOCATIONS') & 
        Key('SK').begins_with('COUNTRY#USA#STATE#CA')
)
Enter fullscreen mode Exit fullscreen mode

This lets a single index answer queries at different granularity levels.

Pattern 4: Adjacency List for Many-to-Many

For many-to-many relationships (e.g., users belonging to multiple groups), the adjacency list pattern models both sides of the relationship:

PK SK
USER#123 GROUP#A
USER#123 GROUP#B
GROUP#A USER#123
GROUP#A USER#456

Query all groups for a user via the base table, and all users in a group by inverting the keys with a GSI.

Pattern 5: Write Sharding for Hot Partitions

If a single partition key receives disproportionate traffic (a "hot partition"), throughput can throttle. Distribute writes by appending a suffix:

import random

shard = random.randint(0, 9)
pk = f"POPULAR_ITEM#{shard}"
Enter fullscreen mode Exit fullscreen mode

To read all data, query each shard and merge results. This trades read complexity for improved write scalability.

Best Practices Summary

  • Minimize table count. Single-table design reduces round trips and operational overhead.
  • Avoid scans. Use Query with well-designed keys instead of Scan on production hot paths.
  • Use on-demand capacity for unpredictable workloads and provisioned with auto-scaling for steady ones.
  • **Enable point

Top comments (0)