Understanding Single Table Design with boto3-assist
Learn how boto3-assist makes DynamoDB single-table design simple and maintainable. Discover the benefits of storing all your entities in one table and how to leverage composite keys for maximum performance.
If you've worked with DynamoDB, you've probably heard about single-table design—the practice of storing all your application's data in a single table rather than creating separate tables for each entity type. While this may seem counterintuitive to those coming from relational databases, it's DynamoDB's recommended best practice and offers significant advantages in terms of performance, cost, and scalability.
In this post, I'll explain the fundamentals of single-table design and show you how boto3-assist makes implementing this pattern straightforward and maintainable.
Why Single Table Design?
Traditional relational database thinking encourages us to create separate tables for each entity type. However, DynamoDB works differently. Here's why single-table design is the recommended approach:
Performance
All related data can be retrieved in a single query, reducing the number of round trips to the database. Instead of making separate requests for an order and its items, you can fetch everything at once.
Cost
Fewer tables mean fewer provisioned resources and lower overall costs. With single-table design, you only pay for one table's read/write capacity instead of multiple tables.
Atomic Transactions
You can perform transactions across multiple entity types within the same table, ensuring data consistency without complex coordination.
Better Data Modeling
Single-table design forces you to think about your access patterns upfront, which leads to more efficient queries and better application architecture.
Traditional vs. Single Table Approach
Traditional Multi-Table Approach:
Users Table: user_id, name, email
Orders Table: order_id, user_id, total
Products Table: product_id, name, price
Single Table Design:
AppTable: pk, sk, ... (all entity attributes)
All your users, orders, and products live in one table, differentiated by their partition and sort keys.
The Foundation: Composite Keys (pk/sk)
In single-table design, the partition key (pk) and sort key (sk) are crucial. They determine:
- Where your data is stored (partition key)
- How your data is organized within that partition (sort key)
- What query patterns you can support
Key Structure Pattern
Keys in boto3-assist follow a structured pattern using the # delimiter:
entityType#entityId
For example:
user#alice- Represents user Aliceproduct#abc-456- Represents product with ID abc-456order#xyz-789- Represents order with ID xyz-789
Simple Entity Storage with boto3-assist
Let's start with the simplest case: storing a single entity type. Here's how to define a Product model:
from boto3_assist.dynamodb.dynamodb_model_base import DynamoDBModelBase
from boto3_assist.dynamodb.dynamodb_index import DynamoDBIndex, DynamoDBKey
class Product(DynamoDBModelBase):
def __init__(self, id=None, name=None, price=0.0):
super().__init__()
self.id = id
self.name = name
self.price = price
self.__setup_indexes()
def __setup_indexes(self):
# PRIMARY KEY: Get product by ID
primary = DynamoDBIndex()
primary.partition_key.attribute_name = "pk"
primary.partition_key.value = lambda: DynamoDBKey.build_key(
("product", self.id)
)
primary.sort_key.attribute_name = "sk"
primary.sort_key.value = lambda: DynamoDBKey.build_key(
("product", self.id)
)
self.indexes.add_primary(primary)
# GSI0: List all products
self.indexes.add_secondary(
DynamoDBIndex(
index_name="gsi0",
partition_key=DynamoDBKey(
attribute_name="gsi0_pk",
value=lambda: DynamoDBKey.build_key(("products", ""))
),
sort_key=DynamoDBKey(
attribute_name="gsi0_sk",
value=lambda: DynamoDBKey.build_key(("name", self.name))
)
)
)
What gets stored in DynamoDB:
{
"pk": "product#abc-123",
"sk": "product#abc-123",
"id": "abc-123",
"name": "Widget",
"price": 29.99
}
Access pattern: Get product by ID
model = Product(id="abc-123")
response = db.get(model=model, table_name=table_name, do_projections=False)
Now you can query all products:
from boto3.dynamodb.conditions import Key
key_condition = Key("gsi0_pk").eq("products")
response = db.query(
key=key_condition,
index_name="gsi0",
table_name=table_name
)
Why boto3-assist Makes This Easy
Without a library like boto3-assist, implementing single-table design involves:
- Manually constructing composite keys for every operation
- Writing repetitive serialization code
- Managing index definitions across your codebase
- Handling DynamoDB's Decimal type conversions
With boto3-assist:
- ✅ Declarative index definitions right in your model class
- ✅ Automatic key generation using lambda functions
- ✅ Type-safe serialization/deserialization with the
map()method - ✅ Automatic Decimal conversion - no more
TypeError: Float types are not supported - ✅ Clean, readable code that clearly expresses your data model
Common Patterns Summary
| Pattern | Partition Key | Sort Key | Use Case |
|---|---|---|---|
| Single Item | entity#id |
entity#id |
Get specific entity |
| One-to-Many | parent#id |
child#id |
Get parent with children |
| All Items | Static value (via GSI) | entity#attribute |
List all of entity type |
What's Next?
In this post, we covered the fundamentals of single-table design and how boto3-assist simplifies the implementation. In my next post, "Modeling One-to-Many Relationships in DynamoDB", I'll show you the real power of single-table design: retrieving an order and all its items in a single query by sharing partition keys.
Get Started Today
boto3-assist is open source and available on PyPI:
pip install boto3-assist
Check out the GitHub repository for more examples and comprehensive documentation.
About the Author: Eric Wilson is the founder of Geek Cafe, specializing in AWS serverless architectures and Python development. He created boto3-assist to simplify common patterns in AWS development.