1961 words
10 minutes
Complete Guide to Amazon DynamoDB: Serverless NoSQL at Scale
Complete Guide to Amazon DynamoDB: Serverless NoSQL at Scale
Amazon DynamoDB is a fully managed, serverless, key-value NoSQL database designed to run high-performance applications at any scale. DynamoDB offers built-in security, continuous backups, automated multi-Region replication, in-memory caching, and data import and export tools.
Overview
DynamoDB provides fast and predictable performance with seamless scalability. You can create database tables that can store and retrieve any amount of data and serve any level of request traffic. DynamoDB automatically spreads your data and traffic for your tables over sufficient servers to handle your throughput and storage requirements.
Key Benefits
1. Performance and Scalability
- Single-digit millisecond latency
- Handles 10 trillion requests per day
- Supports peaks of 20+ million requests per second
- Automatic scaling up and down
2. Serverless
- No servers to manage
- Pay only for what you use
- Automatic scaling with on-demand mode
- Built-in high availability
3. Security and Reliability
- Encryption at rest and in transit
- Fine-grained access control with IAM
- Point-in-time recovery
- Multi-Region replication
4. Developer Productivity
- Simple API operations
- Multiple data types supported
- Flexible schema
- Integration with AWS services
Core Concepts
1. Tables, Items, and Attributes
# Table structure example{ "TableName": "Users", "Items": [ { "userId": {"S": "user123"}, # String "email": {"S": "user@example.com"}, "age": {"N": "30"}, # Number "preferences": { # Map "M": { "theme": {"S": "dark"}, "notifications": {"BOOL": True} } }, "tags": {"SS": ["premium", "beta"]}, # String Set "loginHistory": { # List "L": [ {"S": "2024-01-01T10:00:00Z"}, {"S": "2024-01-02T14:30:00Z"} ] } } ]}2. Primary Keys
# Single Primary Key (Partition Key)UserTable: Type: AWS::DynamoDB::Table Properties: AttributeDefinitions: - AttributeName: userId AttributeType: S KeySchema: - AttributeName: userId KeyType: HASH BillingMode: PAY_PER_REQUEST
# Composite Primary Key (Partition Key + Sort Key)OrderTable: Type: AWS::DynamoDB::Table Properties: AttributeDefinitions: - AttributeName: customerId AttributeType: S - AttributeName: orderDate AttributeType: S KeySchema: - AttributeName: customerId KeyType: HASH - AttributeName: orderDate KeyType: RANGE BillingMode: PAY_PER_REQUEST3. Secondary Indexes
# Global Secondary Index (GSI)ProductTable: Type: AWS::DynamoDB::Table Properties: AttributeDefinitions: - AttributeName: productId AttributeType: S - AttributeName: category AttributeType: S - AttributeName: price AttributeType: N - AttributeName: brand AttributeType: S KeySchema: - AttributeName: productId KeyType: HASH GlobalSecondaryIndexes: - IndexName: CategoryIndex KeySchema: - AttributeName: category KeyType: HASH - AttributeName: price KeyType: RANGE Projection: ProjectionType: ALL BillingMode: PAY_PER_REQUEST - IndexName: BrandIndex KeySchema: - AttributeName: brand KeyType: HASH Projection: ProjectionType: KEYS_ONLY BillingMode: PAY_PER_REQUEST
# Local Secondary Index (LSI)GameScoreTable: Type: AWS::DynamoDB::Table Properties: AttributeDefinitions: - AttributeName: userId AttributeType: S - AttributeName: gameTitle AttributeType: S - AttributeName: topScore AttributeType: N KeySchema: - AttributeName: userId KeyType: HASH - AttributeName: gameTitle KeyType: RANGE LocalSecondaryIndexes: - IndexName: TopScoreIndex KeySchema: - AttributeName: userId KeyType: HASH - AttributeName: topScore KeyType: RANGE Projection: ProjectionType: ALLBasic Operations
1. CRUD Operations
import boto3from botocore.exceptions import ClientError
dynamodb = boto3.resource('dynamodb')table = dynamodb.Table('Users')
# CREATE - Put Itemdef create_user(user_id, email, name): try: response = table.put_item( Item={ 'userId': user_id, 'email': email, 'name': name, 'createdAt': datetime.utcnow().isoformat(), 'status': 'active' }, ConditionExpression='attribute_not_exists(userId)' # Prevent overwrites ) return response except ClientError as e: if e.response['Error']['Code'] == 'ConditionalCheckFailedException': print("User already exists") raise e
# READ - Get Itemdef get_user(user_id): try: response = table.get_item( Key={'userId': user_id}, ProjectionExpression='userId, email, #name, #status', # Only specific attributes ExpressionAttributeNames={ '#name': 'name', # Reserved keyword workaround '#status': 'status' } ) return response.get('Item') except ClientError as e: print(f"Error getting user: {e}") raise e
# UPDATE - Update Itemdef update_user(user_id, email=None, status=None): update_expression = "SET updatedAt = :timestamp" expression_values = {':timestamp': datetime.utcnow().isoformat()}
if email: update_expression += ", email = :email" expression_values[':email'] = email
if status: update_expression += ", #status = :status" expression_values[':status'] = status
try: response = table.update_item( Key={'userId': user_id}, UpdateExpression=update_expression, ExpressionAttributeValues=expression_values, ExpressionAttributeNames={'#status': 'status'}, ConditionExpression='attribute_exists(userId)', # Only update if exists ReturnValues='ALL_NEW' ) return response['Attributes'] except ClientError as e: print(f"Error updating user: {e}") raise e
# DELETE - Delete Itemdef delete_user(user_id): try: response = table.delete_item( Key={'userId': user_id}, ConditionExpression='attribute_exists(userId)', ReturnValues='ALL_OLD' ) return response.get('Attributes') except ClientError as e: print(f"Error deleting user: {e}") raise e2. Query Operations
from boto3.dynamodb.conditions import Key, Attr
# Query with partition keydef get_user_orders(customer_id): response = table.query( KeyConditionExpression=Key('customerId').eq(customer_id) ) return response['Items']
# Query with partition key and sort key rangedef get_orders_by_date_range(customer_id, start_date, end_date): response = table.query( KeyConditionExpression= Key('customerId').eq(customer_id) & Key('orderDate').between(start_date, end_date), ScanIndexForward=False, # Sort in descending order Limit=20 ) return response['Items']
# Query with Global Secondary Indexdef get_products_by_category(category, min_price=None): key_condition = Key('category').eq(category)
if min_price: key_condition &= Key('price').gte(min_price)
response = table.query( IndexName='CategoryIndex', KeyConditionExpression=key_condition, ProjectionExpression='productId, #name, price, description', ExpressionAttributeNames={'#name': 'name'} ) return response['Items']
# Query with filter expressiondef get_active_premium_users(user_type): response = table.query( IndexName='UserTypeIndex', KeyConditionExpression=Key('userType').eq(user_type), FilterExpression= Attr('status').eq('active') & Attr('subscription').eq('premium') & Attr('lastLoginDate').gte('2024-01-01'), ProjectionExpression='userId, email, lastLoginDate' ) return response['Items']3. Scan Operations
# Basic scandef scan_all_users(): response = table.scan( ProjectionExpression='userId, email, #name', ExpressionAttributeNames={'#name': 'name'}, FilterExpression=Attr('status').eq('active') ) return response['Items']
# Parallel scan for large tablesdef parallel_scan_users(total_segments=4): all_items = []
for segment in range(total_segments): response = table.scan( Select='ALL_ATTRIBUTES', TotalSegments=total_segments, Segment=segment, FilterExpression=Attr('status').eq('active') ) all_items.extend(response['Items'])
# Handle pagination while 'LastEvaluatedKey' in response: response = table.scan( Select='ALL_ATTRIBUTES', TotalSegments=total_segments, Segment=segment, ExclusiveStartKey=response['LastEvaluatedKey'], FilterExpression=Attr('status').eq('active') ) all_items.extend(response['Items'])
return all_itemsAdvanced Features
1. Transactions
import boto3from botocore.exceptions import ClientError
dynamodb = boto3.resource('dynamodb')
def transfer_funds(from_account, to_account, amount): try: # Transactional write response = dynamodb.meta.client.transact_write_items( TransactItems=[ { 'Update': { 'TableName': 'Accounts', 'Key': {'accountId': {'S': from_account}}, 'UpdateExpression': 'SET balance = balance - :amount', 'ConditionExpression': 'balance >= :amount', 'ExpressionAttributeValues': { ':amount': {'N': str(amount)} } } }, { 'Update': { 'TableName': 'Accounts', 'Key': {'accountId': {'S': to_account}}, 'UpdateExpression': 'SET balance = balance + :amount', 'ExpressionAttributeValues': { ':amount': {'N': str(amount)} } } }, { 'Put': { 'TableName': 'Transactions', 'Item': { 'transactionId': {'S': str(uuid.uuid4())}, 'fromAccount': {'S': from_account}, 'toAccount': {'S': to_account}, 'amount': {'N': str(amount)}, 'timestamp': {'S': datetime.utcnow().isoformat()} } } } ] ) return {'status': 'success', 'transactionId': response['ItemCollectionMetrics']}
except ClientError as e: if e.response['Error']['Code'] == 'TransactionCanceledException': return {'status': 'failed', 'reason': 'Insufficient funds or account not found'} raise e
def read_transaction_details(transaction_id, account_id): try: # Transactional read response = dynamodb.meta.client.transact_get_items( TransactItems=[ { 'Get': { 'TableName': 'Transactions', 'Key': {'transactionId': {'S': transaction_id}} } }, { 'Get': { 'TableName': 'Accounts', 'Key': {'accountId': {'S': account_id}}, 'ProjectionExpression': 'accountId, balance, #status', 'ExpressionAttributeNames': {'#status': 'status'} } } ] )
return { 'transaction': response['Responses'][0].get('Item'), 'account': response['Responses'][1].get('Item') }
except ClientError as e: print(f"Error reading transaction details: {e}") raise e2. Batch Operations
# Batch write (put/delete multiple items)def batch_create_users(users): table = dynamodb.Table('Users')
with table.batch_writer() as batch: for user in users: batch.put_item(Item=user)
return {'status': 'success', 'count': len(users)}
# Batch get multiple itemsdef batch_get_users(user_ids): response = dynamodb.batch_get_item( RequestItems={ 'Users': { 'Keys': [{'userId': user_id} for user_id in user_ids], 'ProjectionExpression': 'userId, email, #name', 'ExpressionAttributeNames': {'#name': 'name'} } } )
users = response['Responses']['Users']
# Handle unprocessed keys while response.get('UnprocessedKeys'): response = dynamodb.batch_get_item( RequestItems=response['UnprocessedKeys'] ) users.extend(response['Responses']['Users'])
return users3. DynamoDB Streams
# Lambda function to process DynamoDB streamimport json
def lambda_handler(event, context): for record in event['Records']: event_name = record['eventName']
if event_name == 'INSERT': handle_new_item(record['dynamodb']['NewImage']) elif event_name == 'MODIFY': handle_item_update( record['dynamodb']['OldImage'], record['dynamodb']['NewImage'] ) elif event_name == 'REMOVE': handle_item_deletion(record['dynamodb']['OldImage'])
return {'statusCode': 200}
def handle_new_item(new_image): # Process new item creation user_id = new_image['userId']['S'] print(f"New user created: {user_id}")
# Send welcome email, create related records, etc. send_welcome_email(user_id)
def handle_item_update(old_image, new_image): # Process item updates user_id = new_image['userId']['S']
# Check what changed if old_image.get('email', {}).get('S') != new_image.get('email', {}).get('S'): print(f"User {user_id} changed email") # Handle email change logic
def handle_item_deletion(old_image): # Process item deletion user_id = old_image['userId']['S'] print(f"User deleted: {user_id}")
# Cleanup related data, send farewell email, etc.Performance Optimization
1. Capacity Planning
# Provisioned mode with auto scalingTable: Type: AWS::DynamoDB::Table Properties: BillingMode: PROVISIONED ProvisionedThroughput: ReadCapacityUnits: 5 WriteCapacityUnits: 5 GlobalSecondaryIndexes: - IndexName: GSI1 ProvisionedThroughput: ReadCapacityUnits: 5 WriteCapacityUnits: 5
# Auto scaling configurationReadCapacityScalableTarget: Type: AWS::ApplicationAutoScaling::ScalableTarget Properties: ServiceNamespace: dynamodb ResourceId: !Sub "table/${Table}" ScalableDimension: dynamodb:table:ReadCapacityUnits MinCapacity: 1 MaxCapacity: 100
ReadScalingPolicy: Type: AWS::ApplicationAutoScaling::ScalingPolicy Properties: ServiceNamespace: dynamodb ResourceId: !Sub "table/${Table}" ScalableDimension: dynamodb:table:ReadCapacityUnits PolicyName: ReadAutoScalingPolicy PolicyType: TargetTrackingScaling TargetTrackingScalingPolicyConfiguration: TargetValue: 70.0 PredefinedMetricSpecification: PredefinedMetricType: DynamoDBReadCapacityUtilization2. Hot Partition Prevention
import hashlibimport uuid
# Use compound keys to distribute datadef generate_distributed_key(user_id, timestamp): # Add random suffix to partition key hash_suffix = hashlib.md5(f"{user_id}{timestamp}".encode()).hexdigest()[:4] return f"{user_id}#{hash_suffix}"
# Use write sharding for high-frequency countersdef increment_counter(counter_name, increment=1): shard_id = random.randint(0, 9) # 10 shards shard_key = f"{counter_name}#{shard_id}"
table.update_item( Key={'counterId': shard_key}, UpdateExpression='ADD counterValue :inc', ExpressionAttributeValues={':inc': increment} )
def get_counter_total(counter_name): # Sum all shards total = 0 for shard_id in range(10): shard_key = f"{counter_name}#{shard_id}" response = table.get_item(Key={'counterId': shard_key}) if 'Item' in response: total += response['Item'].get('counterValue', 0) return total3. DynamoDB Accelerator (DAX)
# DAX cluster for microsecond latencyDAXCluster: Type: AWS::DAX::Cluster Properties: ClusterName: my-dax-cluster IAMRoleArn: !GetAtt DAXServiceRole.Arn NodeType: dax.r4.large ReplicationFactor: 3 SubnetGroupName: !Ref DAXSubnetGroup SecurityGroupIds: - !Ref DAXSecurityGroup
DAXSubnetGroup: Type: AWS::DAX::SubnetGroup Properties: SubnetGroupName: dax-subnet-group SubnetIds: - !Ref PrivateSubnet1 - !Ref PrivateSubnet2# Using DAX clientimport boto3
# Regular DynamoDB clientdynamodb = boto3.resource('dynamodb')
# DAX client for cachingimport amazondaxdax = amazondax.AmazonDaxClient.resource(endpoint_url='dax://my-dax-cluster.region.dax.amazonaws.com:8111')
def get_user_with_cache(user_id): # Try DAX first (microsecond latency) try: table = dax.Table('Users') response = table.get_item(Key={'userId': user_id}) return response.get('Item') except Exception as e: print(f"DAX error, falling back to DynamoDB: {e}") # Fallback to regular DynamoDB table = dynamodb.Table('Users') response = table.get_item(Key={'userId': user_id}) return response.get('Item')Security Best Practices
1. IAM Policies and Roles
# Fine-grained access controlDynamoDBRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: Service: lambda.amazonaws.com Action: sts:AssumeRole Policies: - PolicyName: DynamoDBAccess PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - dynamodb:GetItem - dynamodb:PutItem - dynamodb:UpdateItem - dynamodb:DeleteItem Resource: !Sub "${UserTable}/index/*" Condition: ForAllValues:StringEquals: dynamodb:LeadingKeys: - "${aws:userid}"2. Encryption
# Encryption at restEncryptedTable: Type: AWS::DynamoDB::Table Properties: SSESpecification: SSEEnabled: true KMSMasterKeyId: !Ref DynamoDBKMSKey
# Customer managed KMS keyDynamoDBKMSKey: Type: AWS::KMS::Key Properties: Description: KMS key for DynamoDB encryption KeyPolicy: Statement: - Effect: Allow Principal: AWS: !Sub "arn:aws:iam::${AWS::AccountId}:root" Action: "kms:*" Resource: "*"3. VPC Endpoints
# VPC endpoint for private accessDynamoDBVPCEndpoint: Type: AWS::EC2::VPCEndpoint Properties: VpcId: !Ref VPC ServiceName: !Sub "com.amazonaws.${AWS::Region}.dynamodb" VpcEndpointType: Gateway RouteTableIds: - !Ref PrivateRouteTableMonitoring and Troubleshooting
1. CloudWatch Metrics and Alarms
# High read throttle alarmReadThrottleAlarm: Type: AWS::CloudWatch::Alarm Properties: AlarmName: DynamoDB-ReadThrottles AlarmDescription: DynamoDB read throttles detected MetricName: ReadThrottleEvents Namespace: AWS/DynamoDB Statistic: Sum Period: 300 EvaluationPeriods: 2 Threshold: 0 ComparisonOperator: GreaterThanThreshold Dimensions: - Name: TableName Value: !Ref UserTable
# Custom metricsdef put_custom_metrics(table_name, operation, latency, success): cloudwatch = boto3.client('cloudwatch')
cloudwatch.put_metric_data( Namespace='DynamoDB/Application', MetricData=[ { 'MetricName': 'OperationLatency', 'Value': latency, 'Unit': 'Milliseconds', 'Dimensions': [ {'Name': 'TableName', 'Value': table_name}, {'Name': 'Operation', 'Value': operation} ] }, { 'MetricName': 'OperationSuccess', 'Value': 1 if success else 0, 'Unit': 'Count', 'Dimensions': [ {'Name': 'TableName', 'Value': table_name}, {'Name': 'Operation', 'Value': operation} ] } ] )2. Error Handling and Retry Logic
import boto3from botocore.exceptions import ClientErrorimport timeimport random
def resilient_dynamodb_operation(operation_func, max_retries=3): """ Wrapper for DynamoDB operations with exponential backoff retry logic """ for attempt in range(max_retries + 1): try: return operation_func()
except ClientError as e: error_code = e.response['Error']['Code']
# Don't retry these errors if error_code in ['ValidationException', 'ResourceNotFoundException']: raise e
# Retry these errors with backoff if error_code in ['ProvisionedThroughputExceededException', 'ThrottlingException']: if attempt < max_retries: # Exponential backoff with jitter wait_time = (2 ** attempt) + random.uniform(0, 1) time.sleep(wait_time) continue else: raise e
# For other errors, raise immediately raise e
raise Exception("Max retries exceeded")
# Usage exampledef safe_get_item(table, key): def operation(): return table.get_item(Key=key)
return resilient_dynamodb_operation(operation)Common Patterns and Use Cases
1. Time Series Data
# Efficient time series storage patterndef store_time_series_data(metric_name, timestamp, value, attributes=None): # Use reverse timestamp for recent-first ordering reverse_timestamp = str(9999999999 - int(timestamp))
item = { 'metricName': metric_name, 'reverseTimestamp': reverse_timestamp, 'timestamp': timestamp, 'value': value, 'ttl': int(timestamp) + (30 * 24 * 60 * 60) # 30 days TTL }
if attributes: item.update(attributes)
table.put_item(Item=item)
def get_recent_metrics(metric_name, limit=100): response = table.query( KeyConditionExpression=Key('metricName').eq(metric_name), ScanIndexForward=True, # Recent first due to reverse timestamp Limit=limit ) return response['Items']2. Leaderboard Pattern
# Gaming leaderboard with efficient queriesdef update_player_score(player_id, game_id, score): # Update player's best score table.update_item( Key={ 'playerId': player_id, 'gameId': game_id }, UpdateExpression='SET bestScore = if_not_exists(bestScore, :zero), bestScore = if_(:score > bestScore, :score, bestScore)', ExpressionAttributeValues={ ':score': score, ':zero': 0 } )
# Also store in GSI optimized for leaderboard queries table.put_item( Item={ 'pk': f"LEADERBOARD#{game_id}", 'sk': f"{str(999999999 - score).zfill(10)}#{player_id}", # Reverse score for top-first ordering 'playerId': player_id, 'gameId': game_id, 'score': score, 'timestamp': datetime.utcnow().isoformat() } )
def get_leaderboard(game_id, limit=10): response = table.query( IndexName='LeaderboardIndex', KeyConditionExpression=Key('pk').eq(f"LEADERBOARD#{game_id}"), ScanIndexForward=True, Limit=limit ) return response['Items']3. Multi-Tenant SaaS Pattern
# Efficient multi-tenant data isolationdef create_tenant_item(tenant_id, item_type, item_id, data): item = { 'pk': f"TENANT#{tenant_id}", 'sk': f"{item_type}#{item_id}", 'tenantId': tenant_id, 'itemType': item_type, 'itemId': item_id, 'data': data, 'createdAt': datetime.utcnow().isoformat() }
table.put_item(Item=item)
def get_tenant_items(tenant_id, item_type=None): key_condition = Key('pk').eq(f"TENANT#{tenant_id}")
if item_type: key_condition &= Key('sk').begins_with(f"{item_type}#")
response = table.query(KeyConditionExpression=key_condition) return response['Items']Additional Resources
Complete Guide to Amazon DynamoDB: Serverless NoSQL at Scale
https://mranv.pages.dev/posts/complete-guide-to-amazon-dynamodb/