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.

✓ Complete✓ Accurate✓ Cost-effective
✗ High latency✗ Hours/days old

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.

✓ Low latency✓ Real-time✓ Responsive
✗ Incomplete✗ Out-of-order

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 SourcesClickstreams, Logs, EventsBatch LayerHistoricalSpeed LayerReal-timeData IngestionImmutableBatch ViewsHDFS / S3Real-time ViewsCassandraServing LayerQuery APIEnd Usersall eventsreal-timeraw datarecomputeupdatemergemergequery

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")
Result: Complete, accurate page view counts up to yesterday. Latency: 12-24 hours.
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()
Result: Approximate page view counts for the last 24 hours. Latency: Seconds.
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']}")
Result: Complete historical data + real-time updates. Best of both worlds!
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

Kinesis StreamsReal-time bufferKinesis FirehoseBatch to S3S3 Data LakeImmutableSpeed LayerKinesis / Lambda / FlinkBatch LayerEMR / Glue / AthenaDynamoDBReal-time viewsS3 + ParquetBatch viewsServing LayerAPI GW + Lambda + AppSyncstreambatchstreambatchwritewritemergemerge

AWS Service Mapping

LayerPurposeAWS ServicesWhy This Service?
IngestionCapture all eventsKinesis Data Streams + FirehoseStreams for real-time, Firehose batches to S3
Batch LayerProcess historical dataEMR (Spark), Glue, AthenaEMR for complex jobs, Glue for ETL, Athena for SQL
Speed LayerReal-time processingKinesis Analytics, Lambda, Flink on EMRAnalytics for SQL, Lambda for events, Flink for complex streaming
Batch StorageStore immutable dataS3 (Parquet/ORC)Cheap, durable, columnar format for analytics
Realtime StorageStore real-time viewsDynamoDB, ElastiCacheLow-latency key-value access for recent data
Serving LayerQuery APIAPI Gateway + Lambda, AppSyncServerless 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

Data SourcesClickstreams, Logs, EventsMessage BrokerKafka / KinesisStream Job #1Views v1Stream Job #2Views v2Stream Job #NNew featureView Store #1View Store #2View Store #NServing APIeventsstreamstreamstreamwritewritewritequeryqueryquery

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 beginning
Key Point: The same code runs in both real-time and replay mode. Just change the Kafka offset. No separate batch layer!
Reprocessing 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 stream
Reprocessing Time: Kafka can replay at 100k+ events/sec. A day's worth of data might take minutes to reprocess, not hours like traditional batch.
Challenge: 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

App EventsProducersKinesis Streams7-365 days retentionStreaming Job v1Kinesis AnalyticsStreaming Job v2ReplayingStreaming Job v3Flink on EMRDynamoDB v1CurrentDynamoDB v2Catching upDynamoDB v3TestingAPI GatewayLambda + Servingproduceconsumereplayconsumewritewritewriteserve

No S3 Data Lake needed - no batch layer - everything is streaming

AWS Service Mapping for Kappa

ComponentAWS ServiceConfigurationWhy?
Event StreamKinesis Data Streams (Enhanced Fan-out)Retention: 365 daysLong retention for replay, enhanced fan-out for multiple consumers
Stream ProcessingKinesis Analytics + Flink, LambdaMultiple jobs, different versionsSQL for simple logic, Flink for complex, Lambda for glue code
State StoreKinesis Analytics Managed StateCheckpointing enabledFault-tolerant state for windowed aggregations
View StorageDynamoDB (per version)On-demand billingLow-latency key-value reads, schema per version
Serving LayerAPI Gateway + LambdaRoute to current versionServerless, handles version routing
Optional ArchiveS3 (via Firehose)Glacier for long-termCheap 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:

DimensionLambda ArchitectureKappa Architecture
Processing LayersBatch + Speed + Serving (3 layers)Stream-only (1 layer)
Code MaintenanceHigh - Duplicate logic in batch and streamLow - Single codebase
Data StorageCheap (S3/HDFS for batch)Expensive (Kafka retention)
ReprocessingNightly batch jobs (hours/days)Stream replay (minutes/hours)
LatencyBatch: Hours, Stream: SecondsUniform: Seconds
AccuracyBatch = 100%, Stream = ApproximateStream = 100% (after replay)
ComplexityHigh - Two systems to manageMedium - One system
Team SkillsNeed Spark AND Flink expertsJust streaming expertise
Best ForMassive scale, complex batch operationsModerate 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.

✓ Solution:

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.

✓ Solution:

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.

✓ Solution:

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.

✓ Solution:

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.

✓ Solution:

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.

✓ Solution:

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()
        raise

Key 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 data

Use 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
Real numbers: LinkedIn processes 7 trillion messages/day with Kafka + Samza. Key: 10,000+ partitions, async I/O, aggressive checkpointing.

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