Lambda & Kappa Architecture
Architectures for processing massive data streams in real-time and batch
The Big Data Processing Challenge
Modern systems generate massive amounts of data, clickstreams, sensor readings, transactions, logs. You need to answer questions like "How many users clicked this button in the last hour?" (real-time) and "What's the total revenue for Q4 2024?" (historical). Lambda and Kappa architectures are two fundamental patterns for solving this dual-processing challenge. Lambda combines batch and streaming layers for completeness and speed. Kappa simplifies by using only streaming. Both enable you to process petabytes of data with low latency, but they make different trade-offs. This lesson covers when to use each, how to implement them with AWS services, and the real-world challenges you'll face.
The Problem: Batch vs Stream Processing
Before Lambda and Kappa, you had to choose between two incompatible approaches:
Batch Processing
Process large volumes of historical data all at once. Accurate and complete, but slow.
Examples: Hadoop MapReduce, Spark batch jobs, nightly ETL
Stream Processing
Process events as they arrive in real-time. Fast and responsive, but may miss data.
Examples: Apache Storm, Flink streaming, Kinesis
The Dilemma
Batch gives you complete, accurate historical analysis. Stream gives you fast, real-time insights. Before Lambda/Kappa, you had to choose one or maintain two separate systems. Lambda and Kappa solve this by unifying both approaches.
Lambda Architecture: The Hybrid Approach
Lambda Architecture combines batch and stream processing into three layers: Batch Layer(processes all historical data), Speed Layer (processes recent data in real-time), and Serving Layer (merges both views). The result: complete, accurate data with low-latency updates.
Lambda Architecture Flow
Data flows through both batch and speed layers before being merged at the serving layer
The Three Layers Explained
Stores all raw data in an immutable, append-only store. Periodically recomputes batch views from scratch using MapReduce/Spark.
Purpose
Complete, accurate, authoritative data
Latency
Hours to days (batch jobs)
Tech
HDFS, S3, Spark, Hadoop
Processes incoming data in real-time to compensate for batch layer latency. Views are approximate and eventually replaced by batch views.
Purpose
Fill the gap until batch catches up
Latency
Milliseconds to seconds
Tech
Storm, Flink, Kinesis, Kafka Streams
Merges batch and real-time views to answer queries. Indexes batch views for fast lookups and combines with speed layer updates.
Purpose
Merge views, serve queries
Latency
Low (indexed reads)
Tech
Cassandra, HBase, DynamoDB, Druid
Key Insight: Query-time Merge
Lambda's superpower is the serving layer's merge function:result = merge(batch_view, realtime_view)This lets you serve complete historical data from batch views while incorporating the last few hours of real-time updates from the speed layer.
Lambda Architecture: Python Implementation
Let's implement a real-time analytics system that tracks page views. The batch layer computes daily totals, the speed layer tracks real-time counts, and the serving layer merges both.
Batch Layer: Process Historical Data
# Batch Layer: Recompute views from all historical data
from pyspark.sql import SparkSession
from datetime import datetime
class BatchLayer:
"""Processes all historical page views to compute accurate counts"""
def __init__(self):
self.spark = SparkSession.builder.appName("BatchLayer").getOrCreate()
def compute_batch_views(self, start_date, end_date):
"""
Run nightly: Reads all page view logs from S3 and computes
accurate page view counts per URL per day.
"""
# Read raw data from immutable storage (S3/HDFS)
df = self.spark.read.parquet(f"s3://data-lake/page_views/{start_date}/*")
# Compute aggregations
batch_view = df.groupBy("url", "date").count()
# Write to serving layer (could be Cassandra, DynamoDB, etc.)
batch_view.write \
.format("org.apache.spark.sql.cassandra") \
.options(table="page_views_batch", keyspace="analytics") \
.mode("append") \
.save()
print(f"Batch view computed for {start_date} to {end_date}")
return batch_view
# Run as nightly batch job
batch_layer = BatchLayer()
batch_layer.compute_batch_views("2024-01-01", "2024-01-31")Speed Layer: Real-time Updates
# Speed Layer: Process streaming data for real-time updates
import json
import boto3
from collections import defaultdict
from datetime import datetime, timedelta, timezone
class SpeedLayer:
"""Processes real-time page views to fill the gap until batch catches up"""
def __init__(self):
self.kinesis = boto3.client('kinesis')
self.dynamodb = boto3.resource('dynamodb')
self.realtime_views = self.dynamodb.Table('page_views_realtime')
# In-memory state (could use Redis for distributed scenarios)
self.window_counts = defaultdict(int)
def process_stream(self):
"""
Consume Kinesis stream: Update real-time counts as events arrive.
These counts are temporary and replaced by batch views.
"""
response = self.kinesis.get_records(
ShardIterator=self.get_shard_iterator(),
Limit=1000
)
for record in response['Records']:
event = json.loads(record['Data'])
self.process_event(event)
def process_event(self, event):
"""Increment real-time counter"""
url = event['url']
timestamp = datetime.fromisoformat(event['timestamp'])
# Only count recent events (last 24 hours)
if datetime.now(timezone.utc) - timestamp < timedelta(hours=24):
key = f"{url}#{timestamp.date()}"
self.window_counts[key] += 1
# Write to DynamoDB for serving layer
self.realtime_views.update_item(
Key={'url_date': key},
UpdateExpression='ADD view_count :inc',
ExpressionAttributeValues={':inc': 1}
)
def cleanup_old_data(self):
"""Remove data older than 24h (batch layer has it now)"""
cutoff = datetime.now(timezone.utc) - timedelta(hours=24)
old_keys = [k for k in self.window_counts.keys()
if datetime.fromisoformat(k.split('#')[1]) < cutoff.date()]
for key in old_keys:
del self.window_counts[key]
# Run continuously
speed_layer = SpeedLayer()
while True:
speed_layer.process_stream()
speed_layer.cleanup_old_data()Serving Layer: Merge & Query
# Serving Layer: Merge batch and real-time views at query time
import boto3
from datetime import datetime, timedelta, timezone
class ServingLayer:
"""Combines batch views (historical) with real-time views (recent)"""
def __init__(self):
self.dynamodb = boto3.resource('dynamodb')
self.batch_table = self.dynamodb.Table('page_views_batch')
self.realtime_table = self.dynamodb.Table('page_views_realtime')
def get_page_views(self, url, start_date, end_date):
"""
Query API: Returns total page views by merging:
- Batch views (complete, up to yesterday)
- Real-time views (approximate, last 24 hours)
"""
total_views = 0
realtime_views = 0
yesterday = datetime.now(timezone.utc).date() - timedelta(days=1)
# 1. Get batch views (older than 24 hours)
batch_views = self.query_batch_views(url, start_date, min(end_date, yesterday))
total_views += batch_views
# 2. Get real-time views (last 24 hours)
if end_date > yesterday:
realtime_views = self.query_realtime_views(url)
total_views += realtime_views
return {
'url': url,
'total_views': total_views,
'batch_views': batch_views,
'realtime_views': realtime_views,
'as_of': datetime.now(timezone.utc).isoformat()
}
def query_batch_views(self, url, start_date, end_date):
"""Query Cassandra/DynamoDB for batch views"""
response = self.batch_table.query(
KeyConditionExpression='url = :url AND date BETWEEN :start AND :end',
ExpressionAttributeValues={
':url': url,
':start': start_date.isoformat(),
':end': end_date.isoformat()
}
)
return sum(item['view_count'] for item in response['Items'])
def query_realtime_views(self, url):
"""Query DynamoDB for real-time views"""
today = datetime.now(timezone.utc).date()
key = f"{url}#{today}"
response = self.realtime_table.get_item(Key={'url_date': key})
return response.get('Item', {}).get('view_count', 0)
# API Usage
serving_layer = ServingLayer()
result = serving_layer.get_page_views(
url="/product/123",
start_date=datetime(2024, 1, 1),
end_date=datetime.now(timezone.utc)
)
print(f"Total views: {result['total_views']}")
print(f" Batch (complete): {result['batch_views']}")
print(f" Real-time (last 24h): {result['realtime_views']}")Challenge: Code Duplication
Notice how batch and speed layers implement the same logic (counting page views) in two different systems. This is Lambda's biggest weakness: maintaining identical business logic in batch (Spark) and streaming (Flink) frameworks. Any bug fix or feature must be implemented twice. This is where Kappa Architecture comes in.
Lambda Architecture on AWS
AWS provides managed services for each Lambda Architecture layer. Here's a typical implementation:
AWS Lambda Architecture Stack
AWS Service Mapping
| Layer | Purpose | AWS Services | Why This Service? |
|---|---|---|---|
| Ingestion | Capture all events | Kinesis Data Streams + Firehose | Streams for real-time, Firehose batches to S3 |
| Batch Layer | Process historical data | EMR (Spark), Glue, Athena | EMR for complex jobs, Glue for ETL, Athena for SQL |
| Speed Layer | Real-time processing | Kinesis Analytics, Lambda, Flink on EMR | Analytics for SQL, Lambda for events, Flink for complex streaming |
| Batch Storage | Store immutable data | S3 (Parquet/ORC) | Cheap, durable, columnar format for analytics |
| Realtime Storage | Store real-time views | DynamoDB, ElastiCache | Low-latency key-value access for recent data |
| Serving Layer | Query API | API Gateway + Lambda, AppSync | Serverless API to merge batch + realtime views |
AWS Lambda Architecture Setup
# AWS Lambda Architecture Infrastructure (Terraform/CloudFormation)
# 1. Kinesis Data Stream (Ingestion)
resource "aws_kinesis_stream" "events" {
name = "analytics-events"
shard_count = 4
retention_period = 168 # 7 days
}
# 2. Firehose to S3 (Batch Layer Feed)
resource "aws_kinesis_firehose_delivery_stream" "s3_delivery" {
name = "analytics-to-s3"
destination = "extended_s3"
extended_s3_configuration {
bucket_arn = aws_s3_bucket.data_lake.arn
prefix = "raw-events/year=!{timestamp:yyyy}/month=!{timestamp:MM}/"
# Buffer for batch writes
buffering_size = 128 # MB
buffering_interval = 300 # seconds (5 min)
}
}
# 3. EMR Cluster (Batch Processing)
resource "aws_emr_cluster" "batch_processing" {
name = "batch-layer"
release_label = "emr-6.10.0"
applications = ["Spark", "Hadoop"]
ec2_attributes {
instance_profile = aws_iam_instance_profile.emr.arn
}
master_instance_group {
instance_type = "m5.xlarge"
}
core_instance_group {
instance_type = "m5.2xlarge"
instance_count = 4
}
}
# 4. Lambda Function (Speed Layer)
resource "aws_lambda_function" "realtime_processor" {
function_name = "realtime-page-views"
runtime = "python3.11"
handler = "lambda_function.handler"
environment {
variables = {
DYNAMODB_TABLE = aws_dynamodb_table.realtime_views.name
}
}
}
# 5. DynamoDB (Real-time Views)
resource "aws_dynamodb_table" "realtime_views" {
name = "page-views-realtime"
billing_mode = "PAY_PER_REQUEST"
hash_key = "url_date"
ttl {
attribute_name = "expiry_time"
enabled = true # Auto-delete after 24h
}
}
# 6. API Gateway (Serving Layer)
resource "aws_api_gateway_rest_api" "serving_api" {
name = "analytics-api"
}
resource "aws_api_gateway_resource" "page_views" {
rest_api_id = aws_api_gateway_rest_api.serving_api.id
parent_id = aws_api_gateway_rest_api.serving_api.root_resource_id
path_part = "page-views"
}When to Use Lambda Architecture
✓ Good Fit
- Historical recomputation needed: You need to reprocess all data when logic changes
- Complex aggregations: Batch jobs are better at complex joins and aggregations
- Accuracy critical: Must guarantee eventual consistency and correctness
- Mixed workloads: Need both ad-hoc queries (batch) and real-time dashboards
- Existing batch infrastructure: Already have Hadoop/Spark, add streaming layer
✗ Poor Fit
- Pure real-time use case: No need for historical reprocessing (use Kappa)
- Small data volume: Overhead isn't worth it for simple analytics
- Fast-changing logic: Maintaining dual code paths becomes a nightmare
- Team size constraints: Need separate Spark and Flink experts
- Strict SLAs: Batch recomputation delays can violate SLAs
Real-World Examples
- E-commerce: Real-time inventory + nightly sales reports
- Social media: Live likes/comments + historical engagement analytics
- Finance: Real-time trading alerts + end-of-day reconciliation
- IoT: Live sensor monitoring + historical trend analysis
Kappa Architecture: Stream-Only Processing
Kappa Architecture simplifies Lambda by eliminating the batch layer entirely. Instead of maintaining separate batch and streaming code, Kappa reprocesses data by replaying the entire event stream. The stream is the source of truth. When you need to recompute, just replay from the beginning. This works because modern streaming platforms like Kafka retain data indefinitely and support fast reprocessing.
Kappa Architecture Flow
Reprocessing: Just deploy new streaming job, replay events from T=0
How Kappa Works
Single Processing Path
Write your business logic once as a streaming job. No duplicate batch/stream implementations. Same code processes both real-time and historical data.
Replayable Event Stream
Store all events in Kafka/Kinesis with long retention (days to weeks). The stream becomes your immutable log. Want historical data? Replay from the beginning.
Reprocessing by Replay
Need to change logic or fix a bug? Deploy a new streaming job and replay events from T=0. New job reads the entire stream, computes views, and becomes the new source.
Materialized Views
Streaming jobs write to materialized view stores (Cassandra, DynamoDB). Queries read these pre-computed views. No merge logic needed, one view per version.
The Big Idea: Stream Replay as Batch
Kappa's innovation is treating stream replay as batch processing. Instead of running nightly Spark jobs on HDFS, you replay Kafka from offset 0. This is only possible because Kafka can store data for weeks and reprocess at high speed. With fast replay, you don't need a separate batch layer.
Kappa Architecture: Python Implementation
Let's implement the same page view analytics system using Kappa. We'll use a single streaming job that processes both real-time events and can replay historical data.
Single Stream Processing Job
# Kappa Architecture: Single streaming job processes everything
from kafka import KafkaConsumer
import boto3
from collections import defaultdict
from datetime import datetime
import json
class KappaStreamProcessor:
"""
Single streaming job that processes ALL page views.
Can run in real-time mode OR replay mode for reprocessing.
"""
def __init__(self, mode='realtime'):
self.mode = mode
self.consumer = KafkaConsumer(
'page-views',
bootstrap_servers=['localhost:9092'],
group_id=f'page-view-processor-{mode}',
auto_offset_reset='earliest' if mode == 'replay' else 'latest',
enable_auto_commit=True
)
self.dynamodb = boto3.resource('dynamodb')
self.views_table = self.dynamodb.Table('page_views')
# State store (checkpointing for fault tolerance)
self.state = defaultdict(int)
def process_stream(self):
"""
Main processing loop: Works for both real-time and replay.
Same code, same logic, different starting offset.
"""
print(f"Starting stream processor in {self.mode} mode...")
for message in self.consumer:
event = json.loads(message.value.decode('utf-8'))
self.process_event(event)
# Checkpoint periodically for fault tolerance
if message.offset % 1000 == 0:
self.checkpoint()
def process_event(self, event):
"""Process a single page view event"""
url = event['url']
timestamp = datetime.fromisoformat(event['timestamp'])
date_key = timestamp.date().isoformat()
# Update in-memory state
key = f"{url}#{date_key}"
self.state[key] += 1
# Update materialized view (DynamoDB)
self.views_table.update_item(
Key={'url_date': key},
UpdateExpression='ADD view_count :inc',
ExpressionAttributeValues={':inc': 1},
ReturnValues='NONE'
)
def checkpoint(self):
"""Save state for fault tolerance"""
# In production, use RocksDB or managed state store
pass
# Real-time processing
realtime_processor = KappaStreamProcessor(mode='realtime')
realtime_processor.process_stream()
# Reprocessing (when logic changes)
replay_processor = KappaStreamProcessor(mode='replay')
replay_processor.process_stream() # Reads from beginningReprocessing Strategy
# Reprocessing in Kappa: Deploy new version, replay stream
class ReprocessingManager:
"""
Manages reprocessing when business logic changes.
Strategy: Blue-green deployment of streaming jobs.
"""
def deploy_new_version(self, version):
"""
1. Deploy new streaming job (v2)
2. Replay Kafka from beginning into new table
3. Switch traffic to new table once caught up
4. Decommission old job (v1)
"""
print(f"Deploying version {version}...")
# Step 1: Create new materialized view table
new_table = self.create_table(f"page_views_v{version}")
# Step 2: Start new streaming job reading from offset 0
new_job = KappaStreamProcessor(mode='replay')
new_job.views_table = new_table
new_job.process_stream() # Processes entire history
# Step 3: Wait until caught up to current time
while not self.is_caught_up(new_job):
time.sleep(60)
# Step 4: Switch serving layer to new table (atomic swap)
self.update_serving_layer(new_table)
# Step 5: Decommission old job
self.stop_old_job()
print(f"Version {version} deployed successfully!")
def is_caught_up(self, job):
"""Check if replay has processed up to current time"""
lag = job.consumer.metrics()['consumer-lag']
return lag < 1000 # Less than 1000 messages behind
# Usage: Reprocess when logic changes
manager = ReprocessingManager()
manager.deploy_new_version(version=2) # Replays entire streamChallenge: Storage Cost
Kappa requires storing the entire event stream in Kafka/Kinesis for weeks or months. This can get expensive at scale (petabytes). Lambda's batch layer uses cheap S3 storage. Trade-off: Kappa pays more for storage to avoid code duplication.
Kappa Architecture on AWS
AWS services for Kappa are simpler than Lambda since there's no batch layer. Focus on streaming infrastructure with long retention.
AWS Kappa Architecture Stack
No S3 Data Lake needed - no batch layer - everything is streaming
AWS Service Mapping for Kappa
| Component | AWS Service | Configuration | Why? |
|---|---|---|---|
| Event Stream | Kinesis Data Streams (Enhanced Fan-out) | Retention: 365 days | Long retention for replay, enhanced fan-out for multiple consumers |
| Stream Processing | Kinesis Analytics + Flink, Lambda | Multiple jobs, different versions | SQL for simple logic, Flink for complex, Lambda for glue code |
| State Store | Kinesis Analytics Managed State | Checkpointing enabled | Fault-tolerant state for windowed aggregations |
| View Storage | DynamoDB (per version) | On-demand billing | Low-latency key-value reads, schema per version |
| Serving Layer | API Gateway + Lambda | Route to current version | Serverless, handles version routing |
| Optional Archive | S3 (via Firehose) | Glacier for long-term | Cheap storage for compliance/auditing |
AWS Kappa Architecture Setup
# AWS Kappa Architecture Infrastructure (Terraform)
# 1. Kinesis Stream with Long Retention (Critical for Kappa)
resource "aws_kinesis_stream" "event_stream" {
name = "analytics-events"
shard_count = 8
retention_period = 8760 # 365 days (max retention)
# Enhanced fan-out for multiple consumers without throttling
stream_mode_details {
stream_mode = "PROVISIONED"
}
}
# 2. Kinesis Analytics Application (Flink Job)
resource "aws_kinesisanalyticsv2_application" "stream_processor" {
name = "page-views-processor-v1"
runtime_environment = "FLINK-1_15"
service_execution_role = aws_iam_role.analytics.arn
application_configuration {
application_code_configuration {
code_content {
s3_content_location {
bucket_arn = aws_s3_bucket.code.arn
file_key = "flink-job.jar"
}
}
code_content_type = "ZIPFILE"
}
# Checkpointing for fault tolerance
flink_application_configuration {
checkpoint_configuration {
configuration_type = "DEFAULT"
checkpointing_enabled = true
checkpoint_interval = 60000 # 1 minute
}
# Parallelism for performance
parallelism_configuration {
parallelism = 8
configuration_type = "CUSTOM"
}
}
}
}
# 3. DynamoDB Table per Version (Blue-Green)
resource "aws_dynamodb_table" "views_v1" {
name = "page-views-v1"
billing_mode = "PAY_PER_REQUEST"
hash_key = "url_date"
attribute {
name = "url_date"
type = "S"
}
# Global secondary index for queries
global_secondary_index {
name = "date-index"
hash_key = "date"
projection_type = "ALL"
}
}
# 4. Lambda for Serving Layer
resource "aws_lambda_function" "query_api" {
function_name = "page-views-query"
runtime = "python3.11"
handler = "lambda_function.handler"
environment {
variables = {
ACTIVE_TABLE = "page-views-v1" # Route to current version
}
}
}
# 5. MSK Cluster (Alternative to Kinesis for Kafka)
# Use this if you need Kafka's ecosystem (Kafka Streams, ksqlDB)
resource "aws_msk_cluster" "kafka" {
cluster_name = "analytics-kafka"
kafka_version = "3.4.0"
number_of_broker_nodes = 3
broker_node_group_info {
instance_type = "kafka.m5.large"
storage_info {
ebs_storage_info {
volume_size = 1000 # 1TB per broker for long retention
}
}
}
# Long retention for replay
configuration_info {
arn = aws_msk_configuration.retention.arn
revision = aws_msk_configuration.retention.latest_revision
}
}
resource "aws_msk_configuration" "retention" {
name = "long-retention"
kafka_versions = ["3.4.0"]
server_properties = <<PROPERTIES
log.retention.hours=8760
log.segment.bytes=1073741824
PROPERTIES
}Cost Warning: Long Retention
Kinesis charges $0.015 per shard-hour + $0.014 per million PUT payloads. With 365-day retention and 8 shards, you're looking at ~$1,000/month just for storage. MSK offers cheaper storage for the same retention. For cost-sensitive workloads, consider Lambda architecture with S3 storage.
When to Use Kappa Architecture
✓ Good Fit
- Pure streaming use case: All processing is incremental, no complex batch joins
- Fast reprocessing: Can replay stream in reasonable time (hours, not days)
- Frequent logic changes: Algorithms change often, reprocessing is common
- Team simplicity: One streaming framework expertise (Flink or Kafka Streams)
- Event-driven system: Events are the source of truth, not database snapshots
- Operational simplicity: Prefer managing one system over two
✗ Poor Fit
- Massive data volume: Storing petabytes in Kafka is prohibitively expensive
- Complex batch operations: Need multi-way joins, graph algorithms, ML training
- Slow replay: Reprocessing would take days/weeks (defeats the purpose)
- Regulatory requirements: Must store raw data separately for compliance
- Mixed workloads: Need both streaming and ad-hoc SQL queries (Lambda is better)
- Cost constraints: Can't afford long Kafka/Kinesis retention
Real-World Examples
- LinkedIn: Used Kappa for real-time newsfeed ranking
- Uber: Uses Kappa for real-time pricing and ETAs
- Netflix: Kappa for real-time recommendations
- Spotify: Kappa for real-time playlist updates
Lambda vs Kappa: Head-to-Head
Choosing between Lambda and Kappa depends on your data volume, processing complexity, team skills, and operational preferences. Here's a detailed comparison:
| Dimension | Lambda Architecture | Kappa Architecture |
|---|---|---|
| Processing Layers | Batch + Speed + Serving (3 layers) | Stream-only (1 layer) |
| Code Maintenance | High - Duplicate logic in batch and stream | Low - Single codebase |
| Data Storage | Cheap (S3/HDFS for batch) | Expensive (Kafka retention) |
| Reprocessing | Nightly batch jobs (hours/days) | Stream replay (minutes/hours) |
| Latency | Batch: Hours, Stream: Seconds | Uniform: Seconds |
| Accuracy | Batch = 100%, Stream = Approximate | Stream = 100% (after replay) |
| Complexity | High - Two systems to manage | Medium - One system |
| Team Skills | Need Spark AND Flink experts | Just streaming expertise |
| Best For | Massive scale, complex batch operations | Moderate scale, simple incremental logic |
Decision Framework
Choose Lambda If...
- Processing petabytes of data
- Need complex batch operations (multi-table joins, graph algorithms)
- Have existing Hadoop/Spark infrastructure
- Batch reprocessing is infrequent (monthly/quarterly)
- Team already knows both batch and streaming
- Need to support ad-hoc SQL queries on historical data
- Storage cost is primary concern (S3 is cheap)
Choose Kappa If...
- Processing terabytes of data (not petabytes)
- Logic is simple and incremental (aggregations, filters)
- Building from scratch (no legacy batch)
- Need to reprocess frequently (weekly/daily)
- Team is streaming-first (Flink/Kafka Streams)
- Want operational simplicity (one system to manage)
- Can replay entire stream in hours (not days)
Hybrid Approach
Some companies use a hybrid: Kappa for real-time metrics (last 7 days), Lambda for long-term historical analysis (years). Archive to S3 after 7 days, use Athena for ad-hoc queries. This balances cost (S3 is cheap) with simplicity (Kappa for hot data).
Common Pitfalls & Solutions
✗ Out-of-Order Events
Network delays cause events to arrive late. Your streaming job processes an event from 10 seconds ago, but an earlier event arrives now.
Use watermarks in Flink/Kafka Streams. Buffer events for a window (e.g. 5 minutes), process when watermark indicates completeness.
✗ Duplicate Processing
Kafka consumer crashes and restarts, reprocessing the same events. This double-counts metrics or charges customers twice.
Make processing idempotent. Use event IDs as deduplication keys in DynamoDB. Or use Kafka transactions for exactly-once semantics.
✗ State Explosion
Streaming job maintains state for millions of users. Memory explodes, job crashes with OOM errors.
Use RocksDB state backend in Flink for disk-backed state. Or time-bound your windows: only keep last 24h of state, not forever.
✗ Schema Evolution
You add a field to your event schema. Old streaming jobs can't parse new events, or vice versa. System breaks.
Use Schema Registry (Confluent/Glue) with Avro/Protobuf. Enforce backward compatibility. Deploy schema changes before code changes.
✗ Backpressure
Consumer can't keep up with producer. Kafka lag grows to millions, system falls hours behind.
Auto-scaling: Increase consumer parallelism (Flink slots, Lambda concurrency). Or add partitions to Kafka. Monitor consumer lag.
✗ Data Loss on Replay
Kafka retention expires before replay finishes. You start reprocessing but events are gone, historical data is lost forever.
Set Kafka retention >replay time + buffer. Or archive to S3 with Tiered Storage (Kafka 2.8+) for infinite retention at low cost.
Monitoring is Critical
Both Lambda and Kappa require extensive monitoring: consumer lag, processing latency, error rates, state size, backpressure. Set up CloudWatch dashboards and alarms for lag >1 hour, error rate >1%, and processing time >p99 latency.
Advanced Topics
Guaranteeing each event is processed exactly once, even during failures. Critical for financial transactions and billing.
# Exactly-once with Kafka Transactions
from kafka import KafkaConsumer, KafkaProducer
consumer = KafkaConsumer(
'orders',
enable_auto_commit=False, # Manual commit
isolation_level='read_committed' # Only read committed transactions
)
producer = KafkaProducer(
transactional_id='order-processor-1' # Unique per instance
)
# Initialize transactions
producer.init_transactions()
for message in consumer:
try:
# Begin transaction
producer.begin_transaction()
# Process event
result = process_order(message.value)
# Write output (within transaction)
producer.send('order-results', result)
# Commit both Kafka offset and output atomically
producer.send_offsets_to_transaction(
{message.topic: {message.partition: message.offset + 1}},
consumer._group_id
)
# Commit transaction (all-or-nothing)
producer.commit_transaction()
except Exception as e:
# Abort on error
producer.abort_transaction()
raiseKey Insight: Transactions group read + processing + write into atomic unit. If any step fails, entire transaction aborts.
Store all state changes as events. Current state is derived by replaying events. Natural fit for Lambda/Kappa.
# Event Sourcing: State from event log
class OrderAggregate:
"""Reconstruct order state from event stream"""
def __init__(self, order_id):
self.order_id = order_id
self.state = {
'status': 'PENDING',
'items': [],
'total': 0
}
def apply_events(self, events):
"""Replay events to rebuild state"""
for event in events:
if event['type'] == 'OrderCreated':
self.state['status'] = 'CREATED'
self.state['items'] = event['items']
elif event['type'] == 'ItemAdded':
self.state['items'].append(event['item'])
self.state['total'] += event['price']
elif event['type'] == 'OrderPaid':
self.state['status'] = 'PAID'
elif event['type'] == 'OrderShipped':
self.state['status'] = 'SHIPPED'
return self.state
# Usage: Rebuild state from Kafka
order = OrderAggregate('order-123')
events = kafka_consumer.get_events(topic='order-events', key='order-123')
current_state = order.apply_events(events)
# Benefits:
# 1. Audit trail: Every change is recorded
# 2. Time travel: Replay to any point in time
# 3. Debuggability: Reproduce any state
# 4. New features: Add projections without migrating dataUse Case: Banking (transaction log), collaboration tools (change history), e-commerce (order lifecycle tracking).
Techniques for processing millions of events per second across thousands of partitions.
- Partition by key: Shard events by user_id or order_id for parallel processing
- Backpressure handling: Use Flink's credit-based flow control to prevent overload
- State sharding: Distribute state across RocksDB instances on disk
- Async I/O: Batch external calls (DB lookups) using Flink's AsyncDataStream
- Windowing: Tumbling windows for aggregations (1-minute windows vs. global state)
- Checkpointing: Snapshot state every N minutes, not every event
- Compression: Use Snappy/LZ4 for Kafka messages to reduce network overhead
Key Takeaways
- Lambda Architecture combines batch (complete, slow) and streaming (fast, approximate) into three layers
- Kappa Architecture simplifies by using only streaming, reprocessing via replay when logic changes
- Trade-offs: Lambda handles massive scale and complex batch ops but requires dual code paths; Kappa is simpler but more expensive for storage
- AWS Lambda stack: Kinesis/MSK → EMR/Glue (batch) + Kinesis Analytics/Flink (stream) → DynamoDB (serving)
- AWS Kappa stack: Kinesis/MSK (long retention) → Flink/Kafka Streams → DynamoDB (versioned views)
- Choose Lambda for petabyte-scale data, complex batch operations, or existing Hadoop infrastructure
- Choose Kappa for terabyte-scale data, simple incremental logic, frequent reprocessing needs, or operational simplicity
- Common pitfalls: Out-of-order events (use watermarks), duplicate processing (idempotency), state explosion (RocksDB), schema evolution (Schema Registry)
- Advanced patterns: Exactly-once with Kafka transactions, Event Sourcing for audit trails, scaling to millions of events/sec with partitioning and async I/O
- Both architectures enable real-time analytics at scale, but the right choice depends on your specific constraints around data volume, processing complexity, team expertise, and operational preferences