Composite keys pattern¶
What is it?¶
Composite keys combine multiple attributes into a single partition or sort key using a delimiter. This pattern enables efficient querying across multiple dimensions without requiring additional indexes.
The pattern uses the format: PART1#PART2#PART3#...
For example: - USER#123#ORDER#456 - Combines user ID and order ID - 2024#12#SALES - Combines year, month, and category - TENANT#acme#USER#alice - Combines tenant and user for multi-tenancy
Why is it important?¶
Query flexibility¶
Composite keys enable hierarchical queries using DynamoDB's beginsWith operator:
// Query all orders for a user
sk: { beginsWith: 'USER#123#ORDER#' }
// Query all sales in December 2024
sk: { beginsWith: '2024#12#' }
Reduced index requirements¶
By encoding multiple attributes in keys, you can support multiple access patterns without creating additional GSIs, reducing costs and complexity.
Hierarchical organization¶
Composite keys naturally represent hierarchical relationships, making your data model intuitive and efficient.
Sort order control¶
You can control the sort order of items by carefully ordering the components in your composite key.
Visual representation¶
Composite Key Structure
graph LR
A[Part 1] -->|#| B[Part 2]
B -->|#| C[Part 3]
C --> D[USER#123#ORDER#456]
style D fill:#4CAF50 Hierarchical query example¶
Querying with Composite Keys
graph TD
Root[Query: beginsWith 'USER#123#']
Root --> O1[USER#123#ORDER#456]
Root --> O2[USER#123#ORDER#789]
Root --> P1[USER#123#PROFILE]
Root --> S1[USER#123#SETTINGS]
style Root fill:#2196F3
style O1 fill:#4CAF50
style O2 fill:#4CAF50
style P1 fill:#FF9800
style S1 fill:#FF9800 Implementation¶
The @ddb-lib/core package provides helper functions for working with composite keys:
Creating composite keys¶
Creating Composite Keys
import { PatternHelpers } from '@ddb-lib/core'
// Simple composite key
const key1 = PatternHelpers.compositeKey(['USER', '123', 'ORDER', '456'])
console.log(key1) // 'USER#123#ORDER#456'
// Time-based composite key
const key2 = PatternHelpers.compositeKey(['2024', '12', 'SALES', 'region-west'])
console.log(key2) // '2024#12#SALES#region-west'
// Multi-tenant composite key
const key3 = PatternHelpers.compositeKey(['TENANT', 'acme', 'USER', 'alice'])
console.log(key3) // 'TENANT#acme#USER#alice'
// Custom separator
const key4 = PatternHelpers.compositeKey(['A', 'B', 'C'], '|')
console.log(key4) // 'A|B|C'
Parsing composite keys¶
Parsing Composite Keys
import { PatternHelpers } from '@ddb-lib/core'
// Parse a composite key
const parts = PatternHelpers.parseCompositeKey('USER#123#ORDER#456')
console.log(parts) // ['USER', '123', 'ORDER', '456']
// Extract specific parts
const [entityType, userId, orderType, orderId] = parts
console.log(userId) // '123'
console.log(orderId) // '456'
// Parse with custom separator
const customParts = PatternHelpers.parseCompositeKey('A|B|C', '|')
console.log(customParts) // ['A', 'B', 'C']
Using with TableClient¶
Composite Keys with TableClient
import { TableClient } from '@ddb-lib/client'
import { PatternHelpers } from '@ddb-lib/core'
const table = new TableClient({
tableName: 'MyTable',
partitionKey: 'pk',
sortKey: 'sk'
})
// Store user order
await table.put({
pk: PatternHelpers.entityKey('USER', '123'),
sk: PatternHelpers.compositeKey(['ORDER', '2024-12-01', '456']),
total: 99.99,
status: 'pending'
})
// Query all orders for a user
const allOrders = await table.query({
keyCondition: {
pk: PatternHelpers.entityKey('USER', '123'),
sk: { beginsWith: 'ORDER#' }
}
})
// Query orders from a specific date
const dateOrders = await table.query({
keyCondition: {
pk: PatternHelpers.entityKey('USER', '123'),
sk: { beginsWith: 'ORDER#2024-12-01#' }
}
})
Common use cases¶
Use case 1: time-series data with categories¶
Time-Series with Categories
import { PatternHelpers } from '@ddb-lib/core'
// Store metrics by time and category
await table.put({
pk: 'METRICS',
sk: PatternHelpers.compositeKey(['2024', '12', '01', 'CPU', 'server-1']),
value: 75.5,
timestamp: Date.now()
})
// Query all CPU metrics for December 1, 2024
const cpuMetrics = await table.query({
keyCondition: {
pk: 'METRICS',
sk: { beginsWith: '2024#12#01#CPU#' }
}
})
// Query all metrics for December 2024
const decemberMetrics = await table.query({
keyCondition: {
pk: 'METRICS',
sk: { beginsWith: '2024#12#' }
}
})
Use case 2: multi-tenancy¶
Multi-Tenant Application
import { PatternHelpers } from '@ddb-lib/core'
// Store tenant-specific data
await table.put({
pk: PatternHelpers.compositeKey(['TENANT', 'acme']),
sk: PatternHelpers.compositeKey(['USER', 'alice', 'PROFILE']),
name: 'Alice Smith',
role: 'admin'
})
await table.put({
pk: PatternHelpers.compositeKey(['TENANT', 'acme']),
sk: PatternHelpers.compositeKey(['USER', 'bob', 'PROFILE']),
name: 'Bob Jones',
role: 'user'
})
// Query all users in a tenant
const tenantUsers = await table.query({
keyCondition: {
pk: 'TENANT#acme',
sk: { beginsWith: 'USER#' }
}
})
// Query specific user in tenant
const userProfile = await table.query({
keyCondition: {
pk: 'TENANT#acme',
sk: { beginsWith: 'USER#alice#' }
}
})
Use case 3: hierarchical categories¶
Product Categories
import { PatternHelpers } from '@ddb-lib/core'
// Store products in hierarchical categories
await table.put({
pk: 'CATALOG',
sk: PatternHelpers.compositeKey(['ELECTRONICS', 'COMPUTERS', 'LAPTOPS', 'product-123']),
name: 'Gaming Laptop',
price: 1299.99
})
// Query all laptops
const laptops = await table.query({
keyCondition: {
pk: 'CATALOG',
sk: { beginsWith: 'ELECTRONICS#COMPUTERS#LAPTOPS#' }
}
})
// Query all computers (laptops, desktops, etc.)
const computers = await table.query({
keyCondition: {
pk: 'CATALOG',
sk: { beginsWith: 'ELECTRONICS#COMPUTERS#' }
}
})
// Query all electronics
const electronics = await table.query({
keyCondition: {
pk: 'CATALOG',
sk: { beginsWith: 'ELECTRONICS#' }
}
})
Use case 4: versioned documents¶
Document Versioning
import { PatternHelpers } from '@ddb-lib/core'
// Store document versions
await table.put({
pk: PatternHelpers.entityKey('DOCUMENT', 'doc-123'),
sk: PatternHelpers.compositeKey(['VERSION', '2024-12-01T10:30:00Z', 'v1']),
content: 'Document content v1',
author: 'alice'
})
await table.put({
pk: PatternHelpers.entityKey('DOCUMENT', 'doc-123'),
sk: PatternHelpers.compositeKey(['VERSION', '2024-12-01T14:20:00Z', 'v2']),
content: 'Document content v2',
author: 'bob'
})
// Query all versions (sorted by timestamp)
const versions = await table.query({
keyCondition: {
pk: 'DOCUMENT#doc-123',
sk: { beginsWith: 'VERSION#' }
}
})
// Query versions from a specific date
const dateVersions = await table.query({
keyCondition: {
pk: 'DOCUMENT#doc-123',
sk: { beginsWith: 'VERSION#2024-12-01#' }
}
})
When to use¶
✅ use composite keys when:¶
- Hierarchical queries: You need to query at different levels of a hierarchy
- Multiple dimensions: Your access patterns involve multiple attributes
- Reducing indexes: You want to avoid creating additional GSIs
- Sort order matters: You need items sorted by multiple attributes
- Time-series data: You're storing time-based data with categories
❌ avoid composite keys when:¶
- Simple access patterns: Single-attribute keys are sufficient
- Frequent updates: Key components change frequently (keys are immutable)
- Complex parsing: You need to frequently parse and manipulate keys
- Very long keys: Too many components make keys unwieldy (DynamoDB has 2KB key limit)
⚠️ considerations:¶
- Key order matters: Put the most selective attributes first for efficient queries
- Separator conflicts: Ensure your data doesn't contain the separator character
- Key immutability: Changing composite keys requires deleting and recreating items
- Query patterns: Design keys based on your query patterns, not your data structure
Best practices¶
1. order components by selectivity¶
// ✅ Good: Most selective first
PatternHelpers.compositeKey(['USER', userId, 'ORDER', orderId])
// Enables: Query all orders for a user
// ❌ Bad: Least selective first
PatternHelpers.compositeKey(['ORDER', orderId, 'USER', userId])
// Can't efficiently query all orders for a user
2. use consistent separators¶
// ✅ Good: Consistent separator throughout application
const separator = '#'
PatternHelpers.compositeKey(['A', 'B', 'C'], separator)
// ❌ Bad: Mixing separators
PatternHelpers.compositeKey(['A', 'B'], '#')
PatternHelpers.compositeKey(['C', 'D'], '|')
3. validate components¶
// ✅ Good: Validate before creating keys
function createOrderKey(userId: string, orderId: string): string {
if (userId.includes('#') || orderId.includes('#')) {
throw new Error('IDs cannot contain # character')
}
return PatternHelpers.compositeKey(['USER', userId, 'ORDER', orderId])
}
4. document key structure¶
// ✅ Good: Document your key patterns
/**
* Sort Key Format: ORDER#{timestamp}#{orderId}
* Enables queries:
* - All orders: beginsWith('ORDER#')
* - Orders by date: beginsWith('ORDER#2024-12-01#')
* - Specific order: equals('ORDER#2024-12-01#456')
*/
const sk = PatternHelpers.compositeKey(['ORDER', timestamp, orderId])
5. use type-safe builders¶
// ✅ Good: Type-safe key builders
interface OrderKeyParts {
userId: string
timestamp: string
orderId: string
}
function buildOrderKey(parts: OrderKeyParts): string {
return PatternHelpers.compositeKey([
'USER',
parts.userId,
'ORDER',
parts.timestamp,
parts.orderId
])
}
function parseOrderKey(key: string): OrderKeyParts {
const [, userId, , timestamp, orderId] = PatternHelpers.parseCompositeKey(key)
return { userId, timestamp, orderId }
}
Performance considerations¶
Query efficiency¶
// ✅ Efficient: Uses key condition
await table.query({
keyCondition: {
pk: 'USER#123',
sk: { beginsWith: 'ORDER#2024-12#' }
}
})
// ❌ Inefficient: Uses filter expression
await table.query({
keyCondition: { pk: 'USER#123' },
filter: {
orderDate: { beginsWith: '2024-12' }
}
})
Key size limits¶
// ⚠️ Watch out: DynamoDB has 2KB limit for keys
// Too many components or long values can exceed this
// ✅ Good: Reasonable key size
PatternHelpers.compositeKey(['USER', '123', 'ORDER', '456'])
// ~20 bytes
// ❌ Bad: Potentially too large
PatternHelpers.compositeKey([
'VERY_LONG_PREFIX',
veryLongId,
'ANOTHER_LONG_PREFIX',
anotherLongId,
// ... many more components
])
Related patterns¶
- Entity Keys - Foundation for composite keys
- Time-Series - Time-based composite keys
- Hierarchical - Hierarchical composite keys
- Multi-Attribute Keys - Advanced composite key management