šŸŽ‰ 75% of content is free forever — Unlock Premium from $10/mo →
CW
šŸ’¼ Servicesā„¹ļø Aboutāœ‰ļø ContactView Pricing Plansfrom $10

DynamoDB Streams for Data Engineers

AWS Data EngineeringDynamoDB Streams & CDC Patterns⭐ Premium

Advertisement

DynamoDB Streams & CDC Patterns

Master change data capture with DynamoDB Streams, Lambda triggers, Kinesis integration, and event-driven architectures.

22 min readAdvanced

Why This Matters

DynamoDB Streams is a native change data capture (CDC) feature built directly into Amazon DynamoDB. Unlike external CDC solutions like Debezium that require additional infrastructure, DynamoDB Streams provides zero-overhead, real-time change tracking that is fully managed by AWS. For data engineers building real-time pipelines, this means you can capture every INSERT, MODIFY, and REMOVE event on a DynamoDB table and route it to Lambda, Kinesis, or other AWS services without any additional infrastructure.

Streams are retained for 24 hours, records are ordered per partition key, and throughput automatically scales with your table's capacity. The feature charges only on Read Request Units consumed when reading stream records, making it cost-effective for both low-volume operational tables and high-throughput analytics workloads.

Architecture Overview

DynamoDB Streams CDC ArchitectureDynamoDB TableINSERT eventsMODIFY eventsREMOVE eventsPer partition key ordering24-hour retentionView: KEYS_ONLY | NEW | OLD | BOTHDynamoDB StreamsShard 1 (pk: user-123)Shard 2 (pk: user-456)Shard 3 (pk: user-789)Auto shard split/mergeSequence numbersIterator-based consumptionAWS LambdaReal-time event processingKinesis Data StreamsExtended retention & fan-outKinesis Data FirehoseDirect S3/Redshift deliveryGlobal TablesCross-region replicationSearch IndexOpenSearchAnalyticsRedshift / AthenaData LakeS3 ParquetReplica TableOther RegionsKey Characteristics24-hour retention1 MB record limitPer-PK orderingAt-least-once deliveryAuto shard managementNo additional throughput costZero infrastructureNative integration

Real-World Project Structure

Architecture Diagram
dynamodb-streams-pipeline/
ā”œā”€ā”€ infrastructure/
│   ā”œā”€ā”€ cdk/
│   │   ā”œā”€ā”€ app.py
│   │   ā”œā”€ā”€ stacks/
│   │   │   ā”œā”€ā”€ dynamodb_stack.py
│   │   │   ā”œā”€ā”€ lambda_triggers_stack.py
│   │   │   ā”œā”€ā”€ kinesis_stack.py
│   │   │   └── monitoring_stack.py
│   │   └── cdk.json
│   └── terraform/
│       ā”œā”€ā”€ main.tf
│       ā”œā”€ā”€ dynamodb.tf
│       ā”œā”€ā”€ lambda_triggers.tf
│       └── variables.tf
ā”œā”€ā”€ lambda_functions/
│   ā”œā”€ā”€ stream_processor/
│   │   ā”œā”€ā”€ app.py
│   │   ā”œā”€ā”€ requirements.txt
│   │   └── tests/
│   ā”œā”€ā”€ firehose_transform/
│   │   ā”œā”€ā”€ app.py
│   │   └── requirements.txt
│   └── index_updater/
│       ā”œā”€ā”€ app.py
│       └── requirements.txt
ā”œā”€ā”€ kinesis/
│   ā”œā”€ā”€ stream_config.json
│   └── firehose_config.json
ā”œā”€ā”€ tests/
│   ā”œā”€ā”€ unit/
│   ā”œā”€ā”€ integration/
│   └── test_stream_processor.py
ā”œā”€ā”€ monitoring/
│   ā”œā”€ā”€ dashboards/
│   └── alarms/
└── README.md

Stream Record View Types

DynamoDB offers four view types that control what data each stream record contains:

View TypeData IncludedUse CaseStorage Cost
KEYS_ONLYPrimary key onlySimple notifications, filteringLowest
NEW_IMAGEItem after modificationReplication, indexingMedium
OLD_IMAGEItem before modificationAudit, cleanupMedium
NEW_AND_OLD_IMAGESBefore and afterDiff detection, CDCHighest

Lambda Trigger Integration

import json
import boto3
from decimal import Decimal

dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('ProcessedItems')

def lambda_handler(event, context):
    """
    Process DynamoDB Stream records from table changes.
    """
    batch_count = 0
    error_count = 0

    for record in event['Records']:
        try:
            event_name = record['eventName']
            dynamodb_data = record['dynamodb']
            seq_num = dynamodb_data['SequenceNumber']

            if event_name == 'INSERT':
                new_image = deserialize(dynamodb_data['NewImage'])
                process_insert(new_image, seq_num)

            elif event_name == 'MODIFY':
                old_image = deserialize(dynamodb_data.get('OldImage', {}))
                new_image = deserialize(dynamodb_data['NewImage'])
                process_modify(old_image, new_image, seq_num)

            elif event_name == 'REMOVE':
                old_image = deserialize(dynamodb_data['OldImage'])
                process_remove(old_image, seq_num)

            batch_count += 1

        except Exception as e:
            print(f"Error processing record: {str(e)}")
            error_count += 1

    print(f"Processed: {batch_count}, Errors: {error_count}")
    return {'processed': batch_count, 'errors': error_count}


def deserialize(image):
    """Convert DynamoDB formatted JSON to regular Python dict."""
    return {k: list(v.values())[0] for k, v in image.items()}


def process_insert(new_image, seq_num):
    """Handle new item creation - index in OpenSearch."""
    item_id = new_image.get('id')
    print(f"INSERT: item={item_id}, seq={seq_num}")
    # Index in OpenSearch for full-text search
    # Notify downstream systems


def process_modify(old_image, new_image, seq_num):
    """Handle item updates - detect specific field changes."""
    item_id = new_image.get('id')
    changed_fields = [
        k for k in new_image
        if k in old_image and old_image[k] != new_image[k]
    ]
    print(f"MODIFY: item={item_id}, changed={changed_fields}, seq={seq_num}")
    # Update search index for changed fields only


def process_remove(old_image, seq_num):
    """Handle item deletion - cleanup downstream resources."""
    item_id = old_image.get('id')
    print(f"REMOVE: item={item_id}, seq={seq_num}")
    # Remove from search index
    # Cleanup related resources

Idempotent Processing

Since DynamoDB Streams provides at-least-once delivery, consumers must be idempotent:

import time
import boto3

dynamodb = boto3.resource('dynamodb')
processed_table = dynamodb.Table('ProcessedEventIds')

def process_record_idempotent(record, handler):
    """Process a stream record with idempotency guarantees."""
    event_id = record['eventID']

    # Check if already processed
    try:
        response = processed_table.get_item(Key={'event_id': event_id})
        if 'Item' in response:
            print(f"Skipping duplicate event: {event_id}")
            return True
    except Exception as e:
        print(f"Error checking idempotency: {str(e)}")

    # Process the record
    handler(record)

    # Mark as processed with conditional write
    try:
        processed_table.put_item(
            Item={
                'event_id': event_id,
                'timestamp': int(time.time()),
                'ttl': int(time.time()) + 86400 * 7  # 7 day TTL
            },
            ConditionExpression='attribute_not_exists(event_id)'
        )
    except dynamodb.meta.client.exceptions.ConditionalCheckFailedException:
        print(f"Concurrent processing detected for: {event_id}")
        return True

    return True

Performance Considerations

MetricValueRecommendation
Record retention24 hoursUse Kinesis for longer retention
Max record size1 MBDesign items to stay under limit
Read throughputMatches table capacityScale table for stream needs
Shard countAuto-managedNo manual intervention needed
Lambda batch sizeUp to 1,000 recordsStart with 100, tune based on latency
Processing latencySub-secondMonitor IteratorAgeMilliseconds

Security Considerations

  • IAM Execution Roles: Grant minimal permissions for stream read and target service access.
  • VPC Endpoints: Use VPC endpoints for DynamoDB and Lambda to keep traffic off public internet.
  • Encryption: DynamoDB Streams encrypts data at rest using AWS-owned keys by default.
  • CloudTrail: Log all DynamoDB API calls including stream enable/disable operations.
  • Lambda Dead Letter Queue: Configure DLQ for failed stream records to prevent data loss.
  • Reserved Concurrent Executions: Limit Lambda concurrency to prevent downstream system overload.
  • Stream Access Control: Use IAM policies to control who can enable/disable streams on tables.

Advanced: DynamoDB Streams with KCL Consumer

For high-throughput scenarios beyond Lambda, use the Kinesis Client Library (KCL) for custom consumers:

from amazon_kclpy import processor
from amazon_kclpy import kcl
import json
import boto3

class StreamRecordProcessor(processor.RecordProcessorBase):
    """
    Custom DynamoDB Streams consumer using KCL.
    """
    def __init__(self):
        self.batch_size = 100
        self.buffer = []

    def initialize(self, shard_id):
        print(f"Processing shard: {shard_id}")

    def process_records(self, records, checkpointer):
        for record in records:
            event_name = record['dynamodb']['NewImage'] and 'INSERT' or 'REMOVE'
            self.buffer.append({
                'event_id': record['eventID'],
                'event_name': event_name,
                'sequence': record['dynamodb']['SequenceNumber']
            })

        if len(self.buffer) >= self.batch_size:
            self.flush_buffer()
            checkpointer.checkpoint(records[-1]['sequenceNumber'])

    def flush_buffer(self):
        # Process buffered records in bulk
        print(f"Processing batch of {len(self.buffer)} records")
        self.buffer = []

    def shutdown(self, checkpointer, reason):
        if reason == 'TERMINATE':
            self.flush_buffer()
            checkpointer.checkpoint()

Advanced: Lambda Event Source Mapping Configuration

import boto3
import json

lambda_client = boto3.client('lambda')

def configure_stream_trigger(function_name, stream_arn):
    """
    Configure Lambda event source mapping for DynamoDB Streams.
    """
    response = lambda_client.create_event_source_mapping(
        FunctionName=function_name,
        EventSourceArn=stream_arn,
        StartingPosition='LATEST',
        BatchSize=100,
        MaximumBatchingWindowInSeconds=60,
        BisectBatchOnFunctionError=True,
        MaximumRetryAttempts=3,
        MaximumRecordAgeInSeconds=86400,  # 24 hours
        TumblingWindowInSeconds=300,  # 5-minute windows
        DestinationConfig={
            'OnFailure': {
                'Destination': 'arn:aws:sqs:us-east-1:123456789012:failed-stream-records'
            }
        },
        FunctionResponseTypes=['ReportBatchItemFailures']
    )

    print(f"Event source mapping created: {response['UUID']}")
    return response['UUID']

Interview Questions & Answers

Q1: What are DynamoDB Streams and when should you use them?

Answer: DynamoDB Streams is a managed CDC feature that captures item-level changes (INSERT, MODIFY, REMOVE) in a DynamoDB table with 24-hour retention. Use streams when you need real-time event-driven processing, data replication, search index updates, or analytics pipelines that react to DynamoDB changes. They are ideal when you want CDC without external tools like Debezium, as the feature is native to DynamoDB with zero additional infrastructure. Streams are particularly powerful for maintaining denormalized views, updating materialized views, and triggering downstream workflows when data changes.

Q2: What are the key differences between DynamoDB Streams and Kinesis Data Streams?

Answer: DynamoDB Streams is per-table, has 24-hour retention, is fully managed by DynamoDB, and captures only DynamoDB changes. Kinesis Data Streams is an independent service with configurable retention (up to 365 days), higher throughput potential, supports multiple producer types beyond DynamoDB, and provides enhanced fan-out. Use DynamoDB Streams for simple, table-specific CDC with minimal operational overhead. Use Kinesis when you need longer retention, higher throughput, or to combine DynamoDB data with other streaming sources in a single pipeline.

Q3: How do you ensure idempotent processing of DynamoDB Stream records?

Answer: Use the eventID field as a unique identifier and track processed events in a deduplication store (DynamoDB table with conditional writes, Redis, etc.). Before processing, check if the event has already been processed. Use conditional writes (ConditionExpression='attribute_not_exists(event_id)') to atomically mark events as processed. Implement TTL on the deduplication records to automatically clean up old entries. This prevents duplicate processing during retries or shard rebalancing, which can cause at-least-once delivery.

Q4: What happens when a DynamoDB Stream record exceeds 1 MB?

Answer: DynamoDB Streams truncates stream records that exceed 1 MB. The truncated record includes a SizeEstimateBytes field indicating the original size. Consumers should detect truncation by checking if the record is smaller than expected, then fetch the complete item directly from DynamoDB using the primary key. Design item access patterns to minimize large items, or implement a fallback read pattern for truncated records. For items that frequently exceed 1 MB, consider splitting them into smaller related items.

Q5: How do DynamoDB Global Tables relate to DynamoDB Streams?

Answer: DynamoDB Global Tables use DynamoDB Streams under the hood for cross-region replication. When you enable Global Tables, DynamoDB automatically creates streams and replicates changes to all participating regions with sub-second latency. This provides multi-active replication where writes to any region are automatically propagated. Global Tables are a managed implementation of the stream-based CDC pattern, abstracting away the complexity of manual replication, conflict resolution, and shard management.

Q6: Explain the difference between TRIM_HORIZON and LATEST as starting positions.

Answer: TRIM_HORIZON starts reading from the oldest available record in the stream (up to 24 hours old). This is useful for backfill scenarios, reprocessing historical changes, or when you need to process all modifications since the stream was enabled. LATEST starts reading from the most recent record, only processing new changes going forward. Choose TRIM_HORIZON for initial load or replay scenarios. Choose LATEST for steady-state processing where you only care about new changes. Both positions maintain their state across Lambda invocations.

Q7: How do you handle shard splits in DynamoDB Streams consumers?

Answer: When a shard splits, the parent shard is closed and child shards are opened. Consumers should detect shard closure via the ShardIterator becoming null or receiving a specific error. At that point, the consumer should start reading from the child shards using the last sequence number from the parent shard. AWS recommends using the Kinesis Client Library (KCL) or Lambda event source mapping which handles shard management automatically. For custom consumers, implement shard iteration tracking and handle child shard discovery gracefully.

Q8: What monitoring should you set up for DynamoDB Streams?

Answer: Monitor CloudWatch metrics including ReadThrottleEvents (indicates capacity issues), GetRecords.IteratorAgeMilliseconds (consumer lag - growing lag means consumers cannot keep up), and ReadProvisionedThroughputExceeded. Set alarms for iterator age growing beyond acceptable thresholds. Log Lambda invocations, errors, and duration to CloudWatch Logs. Track shard count changes which indicate table scaling. Monitor DLQ depth for failed records. Use CloudTrail to audit stream enable/disable operations. Set up dashboards to visualize end-to-end pipeline health.

Common Pitfalls

PitfallImpactSolution
Not implementing idempotencyDuplicate records in downstream systemsUse eventID with conditional writes
Ignoring 1 MB limitTruncated records, missing dataDesign smaller items or implement fallback reads
No DLQ on LambdaFailed records are lostConfigure DLQ for all stream consumers
Wrong view typeMissing before/after imagesChoose NEW_AND_OLD_IMAGES for CDC
Not monitoring iterator ageSilent consumer lagSet CloudWatch alarms on IteratorAgeMilliseconds
Hardcoded batch sizesPerformance issues at scaleStart with 100, tune based on metrics
No TTL on dedup tableDynamoDB costs grow unboundedAdd TTL for automatic cleanup
Assuming global orderingOut-of-order processing bugsDesign for per-partition-key ordering only

Advanced: Kinesis Data Firehose Integration

Route DynamoDB Stream records through Kinesis Data Firehose for direct delivery to S3, Redshift, or OpenSearch:

import json
import base64
import boto3

def lambda_handler(event, context):
    """
    Transform DynamoDB Stream records for Kinesis Data Firehose delivery.
    """
    output = []

    for record in event['Records']:
        # Decode the DynamoDB stream record
        payload = base64.b64decode(record['data']).decode('utf-8')
        dynamodb_record = json.loads(payload)

        # Transform to target format (Parquet-ready JSON)
        transformed = {
            'event_id': record['recordId'],
            'event_name': dynamodb_record['eventName'],
            'timestamp': dynamodb_record['dynamodb']['ApproximateCreationDateTime'],
            'table_name': dynamodb_record['eventSourceARN'].split('/')[1],
            'keys': dynamodb_record['dynamodb']['Keys'],
            'new_image': dynamodb_record['dynamodb'].get('NewImage', {}),
            'old_image': dynamodb_record['dynamodb'].get('OldImage', {}),
            'sequence_number': dynamodb_record['dynamodb']['SequenceNumber']
        }

        # Add partition key for Firehose S3 prefixing
        transformed['dt'] = __import__('datetime').datetime.fromtimestamp(
            transformed['timestamp']
        ).strftime('%Y-%m-%d')

        output.append({
            'recordId': record['recordId'],
            'result': 'Ok',
            'data': base64.b64encode(
                json.dumps(transformed).encode('utf-8')
            ).decode('utf-8')
        })

    return {'records': output}

Advanced: Cross-Region Replication Pattern

import boto3
import json

dynamodb_client = boto3.client('dynamodb')

def setup_global_tables():
    """
    Configure DynamoDB Global Tables for cross-region replication.
    """
    # Global Tables use DynamoDB Streams under the hood
    # This is the managed CDC pattern for multi-region replication
    response = dynamodb_client.create_table(
        TableName='orders-global',
        KeySchema=[
            {'AttributeName': 'order_id', 'KeyType': 'HASH'},
            {'AttributeName': 'timestamp', 'KeyType': 'RANGE'}
        ],
        AttributeDefinitions=[
            {'AttributeName': 'order_id', 'AttributeType': 'S'},
            {'AttributeName': 'timestamp', 'AttributeType': 'N'}
        ],
        BillingMode='PAY_PER_REQUEST',
        GlobalSecondaryIndexes=[
            {
                'IndexName': 'customer-index',
                'KeySchema': [
                    {'AttributeName': 'customer_id', 'KeyType': 'HASH'},
                    {'AttributeName': 'order_date', 'KeyType': 'RANGE'}
                ],
                'Projection': {'ProjectionType': 'ALL'}
            }
        ],
        StreamSpecification={
            'StreamEnabled': True,
            'StreamViewType': 'NEW_AND_OLD_IMAGES'
        }
    )

    # Enable Global Tables replication
    dynamodb_client.update_table(
        TableName='orders-global',
        ReplicaUpdates=[
            {
                'Create': {
                    'RegionName': 'us-west-2',
                    'KMSMasterKeyId': 'alias/aws/dynamodb'
                }
            },
            {
                'Create': {
                    'RegionName': 'eu-west-1',
                    'KMSMasterKeyId': 'alias/aws/dynamodb'
                }
            }
        ]
    )

    print("Global Tables configured for multi-region replication")

Advanced: DynamoDB Streams Analytics

-- Query stream records in Athena for analytics
SELECT
    eventname,
    date_format(
        from_unixtime(dynamodb.approximatecreationdatetime),
        '%Y-%m-%d %H:00:00'
    ) as event_hour,
    COUNT(*) as event_count,
    COUNT(DISTINCT dynamodb.keys['id'].s) as unique_items
FROM dynamodb_streams_table
WHERE dt = '2025-01-15'
GROUP BY 1, 2
ORDER BY event_hour, event_count DESC;

See Also

šŸ”’

Premium Content

DynamoDB Streams for Data Engineers

You've previewed the first section. Unlock this full lesson and 900+ advanced tutorials with a Premium plan.

šŸŽÆEnd-to-end Projects
šŸ’¼Interview Prep
šŸ“œCertificates
šŸ¤Community Access

Already a member? Log in

Advertisement