Apache Flink - Advanced Stream Processing

Advanced stream processing with event time, stateful computations, and exactly-once guarantees

True Stream Processing at Scale

Apache Flink is a distributed stream processing framework designed from the ground up for real-time analytics and event-driven applications. Unlike Spark Streaming's micro-batch approach, Flink processes data as true continuous streams with millisecond latency. It handles event time processing, complex stateful computations, and provides exactly-once consistency guarantees, making it the go-to choice for mission-critical streaming applications like fraud detection, real-time recommendations, and financial trading systems. If your use case demands low latency, sophisticated windowing, and bulletproof correctness, Flink is the answer.

What is Apache Flink?

Apache Flink is a stateful stream processing framework that treats batch processing as a special case of stream processing (finite streams). It was built by the original creators of MapReduce and offers unmatched performance for real-time data pipelines.

Flink Architecture:

┌─────────────────────────────────────────────────────┐
│              Data Sources                           │
│   (Kafka, Kinesis, Files, Sockets, Custom)          │
└───────────────────┬─────────────────────────────────┘
                    │
                    ▼
┌─────────────────────────────────────────────────────┐
│           Flink DataStream API                      │
│   • Event-time processing                           │
│   • Windowing (tumbling, sliding, session)          │
│   • Stateful operations (with checkpoints)          │
│   • Exactly-once semantics                          │
└───────────────────┬─────────────────────────────────┘
                    │
        ┌───────────┴────────────┐
        ▼                        ▼
┌───────────────┐        ┌──────────────┐
│ JobManager    │        │ TaskManagers │
│ (Coordinator) │◄──────►│  (Workers)   │
│               │        │              │
│ • Scheduling  │        │ • Execute    │
│ • Checkpoints │        │   tasks      │
│ • Recovery    │        │ • State      │
└───────────────┘        └──────────────┘
                    │
                    ▼
┌─────────────────────────────────────────────────────┐
│              Data Sinks                             │
│   (Kafka, Databases, Files, Dashboards)             │
└─────────────────────────────────────────────────────┘
Distributed architecture with JobManager coordinating TaskManagers

Key Capabilities

⚡ True Streaming

Process events one at a time with millisecond latency (not micro-batches)

🕒 Event Time

Process events based on when they occurred, not when they arrive

💾 Stateful

Maintain state across billions of events with fault-tolerant checkpoints

✓ Exactly-Once

Guarantee each event is processed exactly once, even with failures

🪟 Advanced Windowing

Tumbling, sliding, session, and custom windows with late data handling

🔄 Savepoints

Snapshot state for versioning, rollbacks, and application upgrades

Flink vs Spark Streaming: Architecture Comparison

The fundamental difference: Flink processes true continuous streams, while Spark Streaming uses micro-batches. This architectural choice cascades into latency, windowing, and state management.

Spark Streaming: Micro-Batch Processing

Treats streaming as a series of small batch jobs

Spark Streaming Model:

Time:     0s         1s         2s         3s         4s
          │          │          │          │          │
Events:   ████       ████       ████       ████       ████
          Batch 1    Batch 2    Batch 3    Batch 4    Batch 5
          │          │          │          │          │
          └──────────┴──────────┴──────────┴──────────┘

          Process each micro-batch using Spark RDDs

Latency:  ~500ms - 2 seconds (limited by batch interval)
Model:    Discrete time intervals, not continuous
State:    Managed at batch boundaries
Collects events into mini-batches, processes as RDDs
Example: Spark Structured Streaming
from pyspark.sql import SparkSession
from pyspark.sql.functions import window, col

spark = SparkSession.builder.appName("SparkStreaming").getOrCreate()

# Read stream from Kafka (micro-batches)
df = spark.readStream \
    .format("kafka") \
    .option("kafka.bootstrap.servers", "localhost:9092") \
    .option("subscribe", "transactions") \
    .load()

# Parse JSON
transactions = df.selectExpr("CAST(value AS STRING)") \
    .selectExpr("from_json(value, schema) as data") \
    .select("data.*")

# Windowed aggregation (processing time by default)
windowed_totals = transactions \
    .groupBy(
        window("timestamp", "1 minute"),  # 1-minute windows
        "user_id"
    ) \
    .agg({"amount": "sum"})

# Write to console (triggers micro-batch processing)
query = windowed_totals.writeStream \
    .outputMode("update") \
    .format("console") \
    .trigger(processingTime="5 seconds")  # Process every 5 seconds \
    .start()

query.awaitTermination()
Result:
Processes transactions in 5-second micro-batches
Latency: 5+ seconds (limited by trigger interval)
Windows aligned to processing time

Apache Flink: True Stream Processing

Processes events continuously, one at a time

Flink Model:

Time:     0s         1s         2s         3s         4s
          │          │          │          │          │
Events:   █─█──█───█─█─█──█───█──█─█────█──█───█─█──

          Each event processed immediately as it arrives

Latency:  ~1ms - 100ms (sub-second)
Model:    Continuous, record-at-a-time processing
State:    Maintained continuously with checkpoints
True streaming: no artificial batching, immediate processing
Example: Flink DataStream API (PyFlink)
from pyflink.datastream import StreamExecutionEnvironment
from pyflink.datastream.window import TumblingEventTimeWindows
from pyflink.common import Time, WatermarkStrategy
from pyflink.common.serialization import SimpleStringSchema
from pyflink.datastream.connectors.kafka import KafkaSource

env = StreamExecutionEnvironment.get_execution_environment()
env.set_parallelism(4)

# Read stream from Kafka (true streaming, not batches)
kafka_source = KafkaSource.builder() \
    .set_bootstrap_servers("localhost:9092") \
    .set_topics("transactions") \
    .set_value_only_deserializer(SimpleStringSchema()) \
    .build()

# Define watermark strategy for event time
watermark_strategy = WatermarkStrategy \
    .for_bounded_out_of_orderness(Time.seconds(5)) \  # 5 sec max lateness
    .with_timestamp_assigner(lambda event: event['timestamp'])

# Create stream with event time
stream = env.from_source(
    kafka_source,
    watermark_strategy,
    "kafka-source"
)

# Parse JSON and extract fields
transactions = stream.map(lambda x: json.loads(x))

# Windowed aggregation using EVENT TIME (not processing time!)
windowed_totals = transactions \
    .key_by(lambda x: x['user_id']) \
    .window(TumblingEventTimeWindows.of(Time.minutes(1))) \  # 1-min event time windows
    .reduce(lambda a, b: {
        'user_id': a['user_id'],
        'total': a['total'] + b['amount']
    })

# Sink to console
windowed_totals.print()

# Execute (streaming job runs continuously)
env.execute("Flink Streaming")
Result:
Processes each transaction immediately (millisecond latency)
Windows based on event timestamps, not arrival time
Handles out-of-order events with watermarks
State maintained continuously with checkpoints

Feature Comparison

FeatureSpark StreamingApache Flink
Processing ModelMicro-batches (discrete intervals)True streaming (continuous)
Latency500ms - 2 seconds1ms - 100ms
Event Time SupportLimited (added in Structured Streaming)Native, first-class support
WindowingBasic (tumbling, sliding)Advanced (tumbling, sliding, session, custom)
State ManagementAt batch boundariesContinuous with checkpoints
Exactly-OnceYes (with structured streaming)Yes (built-in, more mature)
BackpressureLimitedExcellent (credit-based flow control)
Use CaseNear real-time analytics, existing Spark ecosystemLow-latency, complex event processing, mission-critical
Batch + StreamBatch first, streaming addedStreaming first, batch is special case
CommunityLarger (Spark ecosystem)Specialized (streaming experts)
💡 When to Choose:
Spark Streaming: When you already use Spark, need batch + stream, or latency >1 second is acceptable
Flink: When you need sub-second latency, complex event time processing, or sophisticated state management

Event Time Processing & Windowing

Event time processing is Flink's superpower. Instead of processing events based on when they arrive (processing time), Flink uses timestamps embedded in the events themselves, enabling correct results even with out-of-order and late-arriving data.

Processing Time vs Event Time

PROCESSING TIME (arrival time):
Events arrive:    E1(10:00) → E2(10:01) → E3(10:02)
Processed at:     10:05       10:06       10:07
Window:           [10:05-10:06], [10:06-10:07], [10:07-10:08]

Problem: E1 happened at 10:00 but gets windowed at 10:05!
         Results wrong if events delayed or out-of-order


EVENT TIME (when event actually occurred):
Events arrive:    E1(10:00) → E3(10:02) → E2(10:01)  ← Out of order!
Event timestamp:  10:00       10:02       10:01
Window:           [10:00-10:01], [10:01-10:02], [10:02-10:03]

Correct: Each event placed in window based on its actual timestamp
         Results accurate regardless of arrival order or delays
Event time ensures correctness when events are delayed or arrive out-of-order

Watermarks: Tracking Progress in Event Time

Watermarks tell Flink: "I've seen all events up to time T". This allows Flink to know when a window is complete and trigger computations, even with out-of-order data.

Watermark Mechanics:

Time:        10:00   10:01   10:02   10:03   10:04   10:05
             │       │       │       │       │       │
Events:      E1      E3      E2      E5      E4      │
             ts:00   ts:02   ts:01   ts:04   ts:03   │
                                                      │
Watermark:   00      01      02      03      04      │
             │       │       └─Window[00-01] closes  │
             │       │         (E1, E2 included)     │
             │       │                                │
             │       └─Window[01-02] closes          │
             │         (E2, E3 included)             │
             │                                        │
             └─E1 processed immediately               │

Watermark = max(event_time) - allowed_lateness
          = 10:02 - 1 min = 10:01

Window [10:00-10:01] closes when watermark reaches 10:01
Watermarks balance completeness (waiting for late data) vs latency (timely results)
Example: Configuring Watermarks
from pyflink.common import WatermarkStrategy, Time

# Strategy 1: Bounded out-of-orderness
# Assumes events can be up to 5 seconds late
watermark_strategy = WatermarkStrategy \
    .for_bounded_out_of_orderness(Time.seconds(5)) \
    .with_timestamp_assigner(lambda event: event['timestamp'])

# Strategy 2: Monotonous timestamps
# Assumes events always arrive in order (no lateness)
watermark_strategy = WatermarkStrategy \
    .for_monotonous_timestamps() \
    .with_timestamp_assigner(lambda event: event['timestamp'])

# Strategy 3: Custom watermark generator
class CustomWatermarkGenerator(WatermarkGenerator):
    def __init__(self):
        self.max_timestamp = 0

    def on_event(self, event, timestamp):
        # Track maximum timestamp seen
        self.max_timestamp = max(self.max_timestamp, timestamp)

    def on_periodic_emit(self):
        # Emit watermark: max_timestamp - 10 seconds
        return Watermark(self.max_timestamp - 10000)

# Apply watermark to stream
stream = env.from_source(kafka_source, watermark_strategy, "source")
Result:
Watermarks allow Flink to handle late events (up to 5 seconds)
Windows close when watermark passes window end time
Balance: larger lateness = more complete results, higher latency

Window Types

1. Tumbling Windows

Fixed-size, non-overlapping windows. Each event belongs to exactly one window.

Tumbling Window (5 seconds):

Time:  0    5    10   15   20   25   30
       │────│────│────│────│────│────│
       [W1  ][W2  ][W3  ][W4  ][W5  ][W6  ]

Events fall into ONE window only
Use case: Non-overlapping time-based aggregations
Simple, efficient, no overlap between windows
from pyflink.datastream.window import TumblingEventTimeWindows
from pyflink.common import Time

# Count events per 1-minute tumbling window
windowed_counts = stream \
    .key_by(lambda x: x['user_id']) \
    .window(TumblingEventTimeWindows.of(Time.minutes(1))) \
    .reduce(lambda a, b: {
        'user_id': a['user_id'],
        'count': a.get('count', 1) + b.get('count', 1)
    })
Result: Count events per user in 1-minute non-overlapping windows
2. Sliding Windows

Fixed-size windows that slide by a specified interval. Events can belong to multiple windows.

Sliding Window (size=10s, slide=5s):

Time:  0    5    10   15   20   25   30
       │────│────│────│────│────│────│
       [──W1────]
            [──W2────]
                 [──W3────]
                      [──W4────]
                           [──W5────]

Events fall into MULTIPLE overlapping windows
Use case: Moving averages, trend detection
Overlapping windows for continuous analysis
from pyflink.datastream.window import SlidingEventTimeWindows

# Moving average: 5-min window, sliding every 1 minute
moving_avg = stream \
    .key_by(lambda x: x['sensor_id']) \
    .window(SlidingEventTimeWindows.of(
        Time.minutes(5),  # Window size
        Time.minutes(1)   # Slide interval
    )) \
    .reduce(lambda a, b: {
        'sensor_id': a['sensor_id'],
        'avg_temp': (a['avg_temp'] + b['temperature']) / 2
    })
Result: 5-minute moving average updated every 1 minute
3. Session Windows

Dynamic windows based on event gaps. A session closes after a specified inactivity period.

Session Window (gap=5 min):

Events:   E1  E2  E3          E4  E5          E6
Time:     :00 :02 :04         :15 :17         :30
          │───────────│        │─────│         │
          [  Session1 ]        [Sess2]         [S3]

Sessions defined by activity bursts
Use case: User sessions, clickstream analysis
Windows adapt to event patterns dynamically
from pyflink.datastream.window import EventTimeSessionWindows

# User sessions: close after 30 minutes of inactivity
user_sessions = stream \
    .key_by(lambda x: x['user_id']) \
    .window(EventTimeSessionWindows.with_gap(Time.minutes(30))) \
    .reduce(lambda a, b: {
        'user_id': a['user_id'],
        'pages_viewed': a.get('pages_viewed', 1) + 1,
        'session_duration': b['timestamp'] - a['start_time']
    })
Result: Track user sessions that close after 30 min of inactivity

Handling Late Data

What happens when events arrive after their window has closed? Flink provides side outputs for late data.

from pyflink.datastream.window import TumblingEventTimeWindows
from pyflink.common import Time
from pyflink.datastream import OutputTag

# Define late data tag
late_data_tag = OutputTag("late-data", Types.STRING())

# Configure window with allowed lateness
windowed_stream = stream \
    .key_by(lambda x: x['user_id']) \
    .window(TumblingEventTimeWindows.of(Time.minutes(1))) \
    .allowed_lateness(Time.minutes(5)) \  # Allow 5 min late data
    .side_output_late_data(late_data_tag) \  # Send late data to side output
    .reduce(lambda a, b: {
        'user_id': a['user_id'],
        'total': a['total'] + b['amount']
    })

# Get late events from side output
late_events = windowed_stream.get_side_output(late_data_tag)

# Process late events separately (log, store for reprocessing, etc.)
late_events.print("LATE EVENT")
Result:
Events up to 5 minutes late are included in window
Events more than 5 minutes late go to side output
Prevents indefinite waiting while handling stragglers

Stateful Computations at Scale

Flink's state management allows operations to remember information across events. This enables complex patterns like aggregations, joins, machine learning models, and fraud detection that need to track state for millions of keys (users, devices, sessions).

Types of State

Keyed State

State scoped to a specific key (user, device, session)

• ValueState: Single value per key
• ListState: List of values per key
• MapState: Key-value map per key
• ReducingState: Aggregated value per key
Operator State

State scoped to an operator instance (not keyed)

• ListState: List accessible by operator
• UnionListState: Merged on restore
• BroadcastState: Shared across all parallel instances

Example: Fraud Detection with Keyed State

Track transaction amounts per user to detect suspicious patterns.

from pyflink.datastream import StreamExecutionEnvironment, KeyedProcessFunction
from pyflink.common.typeinfo import Types
from pyflink.datastream.state import ValueStateDescriptor

class FraudDetector(KeyedProcessFunction):
    """Detect fraud: alert if 2 large transactions within 1 minute"""

    def open(self, runtime_context):
        # Initialize state: last large transaction timestamp
        self.last_large_tx_state = runtime_context.get_state(
            ValueStateDescriptor("last-large-tx", Types.LONG())
        )

    def process_element(self, transaction, ctx):
        # Check if transaction is large (> $1000)
        if transaction['amount'] > 1000:
            # Get last large transaction time
            last_large_tx_time = self.last_large_tx_state.value()

            if last_large_tx_time is not None:
                # Check if within 1 minute
                time_diff = transaction['timestamp'] - last_large_tx_time
                if time_diff < 60000:  # 60 seconds in milliseconds
                    # FRAUD DETECTED!
                    yield {
                        'user_id': transaction['user_id'],
                        'alert': 'FRAUD',
                        'reason': f'Two large transactions within {time_diff/1000}s',
                        'amount1': last_large_tx_time,
                        'amount2': transaction['amount']
                    }

            # Update state with current transaction time
            self.last_large_tx_state.update(transaction['timestamp'])

# Apply fraud detector
env = StreamExecutionEnvironment.get_execution_environment()

transactions = env.from_collection([
    {'user_id': 'U123', 'amount': 1500, 'timestamp': 1000},
    {'user_id': 'U123', 'amount': 2000, 'timestamp': 30000},  # 29s later - FRAUD!
    {'user_id': 'U456', 'amount': 500, 'timestamp': 5000},
    {'user_id': 'U123', 'amount': 3000, 'timestamp': 120000}, # >1min later - OK
])

alerts = transactions \
    .key_by(lambda x: x['user_id']) \
    .process(FraudDetector())

alerts.print()
env.execute("Fraud Detection")
Result:
User U123: Alert triggered for 2nd large transaction (29 seconds apart)
State maintained per user_id across all events
Scales to millions of users with efficient state backend

State Backends: Where State Lives

Flink stores state in backends that balance performance and durability.

BackendStoragePerformanceUse Case
MemoryStateBackendHeap memoryFastestTesting, small state
FsStateBackendHeap + file system checkpointsFastMedium state (< few GB)
RocksDBStateBackendRocksDB (disk) + distributed storageSlower, scalableLarge state (TBs), production
from pyflink.datastream import StreamExecutionEnvironment

env = StreamExecutionEnvironment.get_execution_environment()

# Configure RocksDB state backend (for large state)
env.set_state_backend(
    "rocksdb",
    checkpoint_path="s3://my-bucket/checkpoints"
)

# Enable checkpointing every 60 seconds
env.enable_checkpointing(60000)  # milliseconds
Result: State stored in RocksDB, checkpointed to S3 every 60s

Checkpoints vs Savepoints

Checkpoints

Purpose: Automatic fault tolerance

  • Triggered automatically (e.g., every 60s)
  • Lightweight snapshots
  • Used for failure recovery
  • Deleted after job stops
  • Not portable across Flink versions
Savepoints

Purpose: Manual operational snapshots

  • Triggered manually by operator
  • Full state snapshot
  • Used for upgrades, rollbacks, A/B testing
  • Persist indefinitely
  • Portable (version compatible)
# Create a savepoint manually (via CLI)
$ flink savepoint <job-id> s3://my-bucket/savepoints

# Stop job with savepoint
$ flink stop --savepointPath s3://my-bucket/savepoints <job-id>

# Restore from savepoint (e.g., after code upgrade)
$ flink run -s s3://my-bucket/savepoints/savepoint-123 my-job.jar
Result: Savepoint enables zero-downtime upgrades and rollbacks

Exactly-Once Semantics

Flink guarantees each event is processed exactly once, even with failures. This is critical for financial transactions, billing, and any use case where duplicates or data loss is unacceptable.

Processing Guarantees Explained

At-Most-Once

Events may be lost but never duplicated

Example: Logs, metrics (data loss OK)
Problem: Missing data
At-Least-Once

Events never lost but may be duplicated

Example: Alert systems (duplicates OK)
Problem: Duplicates inflate counts
Exactly-Once

Each event processed once, no loss, no duplicates

Example: Financial transactions, billing
Benefit: Guaranteed correctness

How Exactly-Once Works in Flink

Flink achieves exactly-once through a combination of distributed snapshots (checkpoints) and transactional sinks.

Flink's Exactly-Once Mechanism:

1. Distributed Snapshots (Chandy-Lamport Algorithm):
   ┌─────────────────────────────────────────────┐
   │  Checkpoint Barrier flows through pipeline  │
   │                                             │
   │  Source ─barrier→ Op1 ─barrier→ Op2 ─Sink   │
   │    │               │         │              │
   │    └─ state ───────┴─ state ┴──── state     │
   │                                             │
   │  When barrier reaches operator:             │
   │    • Snapshot state                         │
   │    • Forward barrier                        │
   └─────────────────────────────────────────────┘

2. Two-Phase Commit for Sinks:
   ┌─────────────────────────────────────────────┐
   │  Checkpoint N started:                      │
   │    Phase 1 (Pre-commit): Stage writes       │
   │                                             │
   │  Checkpoint N completes:                    │
   │    Phase 2 (Commit): Atomically commit      │
   └─────────────────────────────────────────────┘

3. On Failure:
   • Restore state from last successful checkpoint
   • Replay events from checkpoint onwards
   • No duplicates or data loss
Checkpoint barriers coordinate snapshots across distributed operators

Enabling Exactly-Once

from pyflink.datastream import StreamExecutionEnvironment, CheckpointingMode

env = StreamExecutionEnvironment.get_execution_environment()

# Enable checkpointing with exactly-once mode
env.enable_checkpointing(60000, CheckpointingMode.EXACTLY_ONCE)

# Configure checkpoint settings
checkpoint_config = env.get_checkpoint_config()

# Checkpoint timeout (max time for checkpoint to complete)
checkpoint_config.set_checkpoint_timeout(300000)  # 5 minutes

# Minimum time between checkpoints (avoid too frequent)
checkpoint_config.set_min_pause_between_checkpoints(30000)  # 30 seconds

# Max concurrent checkpoints
checkpoint_config.set_max_concurrent_checkpoints(1)

# Retain checkpoints on job cancellation (for recovery)
checkpoint_config.enable_externalized_checkpoints(
    ExternalizedCheckpointCleanup.RETAIN_ON_CANCELLATION
)

# Configure Kafka sink with exactly-once (requires Kafka 0.11+)
from pyflink.datastream.connectors.kafka import KafkaSink, KafkaRecordSerializationSchema

kafka_sink = KafkaSink.builder() \
    .set_bootstrap_servers("localhost:9092") \
    .set_record_serializer(
        KafkaRecordSerializationSchema.builder()
            .set_topic("output-topic")
            .set_value_serialization_schema(SimpleStringSchema())
            .build()
    ) \
    .set_delivery_guarantee(DeliveryGuarantee.EXACTLY_ONCE) \  # Exactly-once!
    .set_transactional_id_prefix("flink-txn-") \
    .build()

# Write to Kafka with exactly-once guarantees
stream.sink_to(kafka_sink)

env.execute("Exactly-Once Pipeline")
Result:
Checkpoints every 60 seconds with exactly-once mode
Kafka sink uses two-phase commit for exactly-once delivery
On failure, job recovers from last checkpoint with no duplicates or data loss

Requirements for End-to-End Exactly-Once

⚠️ Important: Exactly-once requires cooperation from sources and sinks
✅ Sources
  • Must be replayable (Kafka offsets, file positions)
  • Flink resets source to checkpoint position on recovery
  • Supported: Kafka, Kinesis, file systems
✅ Sinks
  • Must support transactions or idempotent writes
  • Two-phase commit (Kafka, databases with ACID)
  • Supported: Kafka, Postgres, MySQL, file systems

Complete Example: Real-Time Analytics Pipeline

Let's build a complete Flink pipeline: ingest clickstream data, detect user sessions, calculate metrics, and sink to Kafka with exactly-once guarantees.

from pyflink.datastream import StreamExecutionEnvironment, KeyedProcessFunction, CheckpointingMode
from pyflink.datastream.window import EventTimeSessionWindows, TumblingEventTimeWindows
from pyflink.common import WatermarkStrategy, Time, Types
from pyflink.common.serialization import SimpleStringSchema
from pyflink.datastream.connectors.kafka import KafkaSource, KafkaSink, KafkaRecordSerializationSchema, DeliveryGuarantee
from pyflink.datastream.state import ValueStateDescriptor
import json

env = StreamExecutionEnvironment.get_execution_environment()
env.set_parallelism(4)

# Enable checkpointing (exactly-once)
env.enable_checkpointing(60000, CheckpointingMode.EXACTLY_ONCE)

# ============ 1. INGEST FROM KAFKA ============
kafka_source = KafkaSource.builder() \
    .set_bootstrap_servers("localhost:9092") \
    .set_topics("clickstream") \
    .set_group_id("flink-analytics") \
    .set_value_only_deserializer(SimpleStringSchema()) \
    .build()

# Watermark: 5 seconds max lateness
watermark_strategy = WatermarkStrategy \
    .for_bounded_out_of_orderness(Time.seconds(5)) \
    .with_timestamp_assigner(lambda event: json.loads(event)['timestamp'])

clickstream = env.from_source(kafka_source, watermark_strategy, "clickstream-source")

# ============ 2. PARSE & ENRICH ============
def parse_click(event_json):
    event = json.loads(event_json)
    return {
        'user_id': event['user_id'],
        'page': event['page'],
        'timestamp': event['timestamp'],
        'session_id': None  # Will be set by session window
    }

clicks = clickstream.map(parse_click)

# ============ 3. SESSION DETECTION ============
# Group into sessions: 30 min inactivity timeout
user_sessions = clicks \
    .key_by(lambda x: x['user_id']) \
    .window(EventTimeSessionWindows.with_gap(Time.minutes(30))) \
    .reduce(lambda a, b: {
        'user_id': a['user_id'],
        'pages_viewed': a.get('pages_viewed', 1) + 1,
        'start_time': min(a.get('start_time', a['timestamp']), b['timestamp']),
        'end_time': max(a.get('end_time', a['timestamp']), b['timestamp']),
        'duration': b['timestamp'] - a.get('start_time', a['timestamp'])
    })

# ============ 4. ANOMALY DETECTION ============
class AnomalyDetector(KeyedProcessFunction):
    """Alert if session duration > 2 hours (unusual)"""

    def open(self, runtime_context):
        self.avg_duration_state = runtime_context.get_state(
            ValueStateDescriptor("avg-duration", Types.FLOAT())
        )

    def process_element(self, session, ctx):
        duration_minutes = session['duration'] / 60000

        # Get running average
        avg_duration = self.avg_duration_state.value()
        if avg_duration is None:
            avg_duration = duration_minutes
        else:
            # Exponential moving average
            avg_duration = 0.9 * avg_duration + 0.1 * duration_minutes

        # Update state
        self.avg_duration_state.update(avg_duration)

        # Detect anomaly
        if duration_minutes > 120:  # > 2 hours
            yield {
                'type': 'ANOMALY',
                'user_id': session['user_id'],
                'duration_minutes': duration_minutes,
                'avg_duration_minutes': avg_duration,
                'deviation': duration_minutes / avg_duration if avg_duration > 0 else 0
            }
        else:
            yield {
                'type': 'NORMAL',
                'user_id': session['user_id'],
                'duration_minutes': duration_minutes,
                'pages_viewed': session['pages_viewed']
            }

alerts = user_sessions \
    .key_by(lambda x: x['user_id']) \
    .process(AnomalyDetector())

# ============ 5. AGGREGATED METRICS ============
# Count sessions per 5-minute tumbling window
session_counts = user_sessions \
    .window(TumblingEventTimeWindows.of(Time.minutes(5))) \
    .reduce(lambda a, b: {
        'window': 'count',
        'total_sessions': a.get('total_sessions', 1) + 1,
        'total_pages': a.get('total_pages', a['pages_viewed']) + b['pages_viewed']
    })

# ============ 6. SINK TO KAFKA (EXACTLY-ONCE) ============
kafka_sink = KafkaSink.builder() \
    .set_bootstrap_servers("localhost:9092") \
    .set_record_serializer(
        KafkaRecordSerializationSchema.builder()
            .set_topic("analytics-output")
            .set_value_serialization_schema(
                lambda x: json.dumps(x).encode('utf-8')
            )
            .build()
    ) \
    .set_delivery_guarantee(DeliveryGuarantee.EXACTLY_ONCE) \
    .set_transactional_id_prefix("flink-analytics-") \
    .build()

# Write both alerts and metrics
alerts.sink_to(kafka_sink)
session_counts.sink_to(kafka_sink)

# ============ 7. EXECUTE ============
env.execute("Real-Time Clickstream Analytics")
Complete Pipeline:
1. Ingest clickstream from Kafka with event time
2. Detect user sessions (30-min inactivity timeout)
3. Detect anomalies (sessions >2 hours) using stateful computation
4. Calculate aggregated metrics (sessions per 5-min window)
5. Write to Kafka with exactly-once guarantees
6. Handles out-of-order events, failures, and provides sub-second latency

Bonus: Real-Time E-Commerce Analytics with PyFlink

Every concept from this lesson: event time, watermarks, tumbling and sliding windows, RocksDB state backend, and exactly-once checkpointing, is demonstrated in a working project using PyFlink 2.2, Kafka (KRaft), and PostgreSQL. A single Flink job runs three concurrent aggregations as one StatementSet, all reading from the same Kafka source, and writes results to PostgreSQL via JDBC upsert. A Streamlit dashboard reads directly from Kafka for a live view without touching the database.

The three aggregations:

  • revenue_by_category: 1-minute tumbling window: total revenue and order count per product category per minute
  • product_performance: 5-minute HOP window with 1-minute slide: rolling revenue per product, each event appears in 5 consecutive windows for a smooth trend line
  • user_order_velocity: 5-minute tumbling window with a fraud flag: per-user order count, spend, and max order amount; is_suspicious = TRUE when order_count > 10 or max_order_amount > $1,000 in any 5-minute window
# Prerequisites: Docker >= 24, Python 3.12, make
git clone https://gitlab.com/bytecode-solutions/examples/flink-streaming-processing
cd flink-streaming-processing

python3.12 -m venv .venv && source .venv/bin/activate

make install   # pins setuptools=70 to work around apache-beam/pkg_resources issue
make build     # custom Flink image with connector JARs baked in
make up        # Flink Web UI → :8081 | Kafka UI → :8080

make submit    # submit the Flink job (detached, runs on cluster)

# Produce order events from your laptop
make produce         # 10 msg/s, 2% fraud rate
make produce-fraud   # 30 msg/s, 100% fraud – triggers fraud window immediately

# Live Streamlit dashboard (reads from Kafka, no DB required)
make dashboard       # → http://localhost:8501

# Query results in PostgreSQL
make psql
# analytics=# SELECT * FROM revenue_by_category ORDER BY window_end DESC LIMIT 10;
# analytics=# SELECT * FROM user_order_velocity WHERE is_suspicious = TRUE;

# Run tests and linters
make test   # pytest -v tests/ (validates DDL + event shapes without a live cluster)
make lint   # ruff + mypy

Key Takeaways

  • Flink processes true continuous streams (not micro-batches)
  • Event time enables correct results with out-of-order data
  • Watermarks track progress in event time and trigger windows
  • Windowing supports tumbling, sliding, session, and custom patterns
  • Stateful computations scale to millions of keys with checkpoints
  • Exactly-once guarantees correctness for mission-critical use cases
  • RocksDB backend enables TBs of state
  • Savepoints allow zero-downtime upgrades and rollbacks
Remember: Choose Flink when you need sub-second latency, sophisticated event time processing, or bulletproof exactly-once guarantees. It excels at complex stateful computations like fraud detection, real-time ML feature engineering, and mission-critical financial systems. The learning curve is steeper than Spark Streaming, but the capabilities are unmatched for advanced stream processing.