System Design

Build Real-Time Streaming Pipelines with Kafka & Flink

Learn to build robust, scalable real-time streaming pipelines using Apache Kafka for data ingestion and Apache Flink for powerful stateful processing. Includes code examples.

Khader Vali August 10, 2026 15 min read

Building Real-Time Streaming Pipelines with Apache Kafka and Apache Flink

In today’s data-driven world, the ability to process and react to data in real-time is no longer a luxury but a necessity. From fraud detection and personalized recommendations to IoT analytics and real-time operational monitoring, businesses demand immediate insights from their ever-growing streams of information. Batch processing, while still valuable for historical analysis, simply can’t keep up with the velocity and volume of modern data. This is where real-time streaming pipelines come into play, transforming raw events into actionable intelligence as they happen.

At the heart of many high-performance streaming architectures are two open-source powerhouses: Apache Kafka and Apache Flink. Kafka provides the reliable, scalable backbone for ingesting and transporting billions of events per day, while Flink offers a powerful, stateful engine for processing those events with millisecond latency, exactly-once semantics, and fault tolerance.

As a senior engineer who’s built and optimized numerous data pipelines, I’ve seen firsthand the transformative power of this combination. In this comprehensive guide, we’ll dive deep into building real-time streaming pipelines using Kafka for data ingestion and Flink for sophisticated stream processing. We’ll cover their core concepts, explore their synergy, walk through a practical real-world scenario with code examples, and discuss best practices for deploying and managing these systems.

The Foundation: Apache Kafka – The Distributed Streaming Platform

Think of Apache Kafka as a high-throughput, distributed, fault-tolerant commit log. It’s designed to handle streams of records, allowing producers to write data and consumers to read it, all while ensuring durability and scalability. It acts as a central nervous system for your data, decoupling data producers from data consumers and allowing multiple applications to process the same data stream independently.

Core Concepts of Kafka

  • Producers: Applications that publish (write) records to Kafka topics.
  • Consumers: Applications that subscribe to (read) records from Kafka topics.
  • Topics: Categories or feed names to which records are published. Topics are logically partitioned.
  • Partitions: A topic is divided into an ordered, immutable sequence of records called partitions. Each record in a partition is assigned a sequential ID number called an offset. Partitions enable parallelism and scalability.
  • Brokers: Kafka servers that store the published data. A Kafka cluster consists of one or more brokers.
  • Zookeeper: (Though being phased out in newer versions in favor of KRaft) Historically, Zookeeper was used by Kafka for managing and coordinating brokers, leader election, and storing metadata.

Why Kafka for Real-Time Pipelines?

  • High Throughput: Capable of handling millions of messages per second.
  • Scalability: Easily scale out by adding more brokers and partitions.
  • Durability: Data is persisted to disk and replicated across multiple brokers, preventing data loss.
  • Fault Tolerance: If a broker fails, other replicas can take over.
  • Decoupling: Producers and consumers are independent, allowing systems to evolve separately.
  • Ordered Delivery: Guarantees message order within a partition.

Basic Kafka Producer and Consumer Example (Python)

Let’s illustrate how to send and receive messages with Kafka using Python’s kafka-python library. First, ensure you have Kafka running (e.g., via Docker or a local installation) and the library installed (pip install kafka-python).

Kafka Producer


from kafka import KafkaProducer
import json
import time

# Configuration for Kafka broker
KAFKA_BROKER = 'localhost:9092'
TOPIC_NAME = 'my_realtime_events'

# Initialize Kafka Producer
# value_serializer converts your data to bytes before sending
producer = KafkaProducer(
    bootstrap_servers=[KAFKA_BROKER],
    value_serializer=lambda v: json.dumps(v).encode('utf-8')
)

print(f"Sending messages to topic: {TOPIC_NAME}")

for i in range(10):
    message = {'event_id': i, 'timestamp': time.time(), 'data': f'Test message {i}'}
    producer.send(TOPIC_NAME, value=message)
    print(f"Sent: {message}")
    time.sleep(1) # Simulate real-time stream

producer.flush() # Ensure all messages are sent
producer.close()
print("Producer finished sending messages.")

Kafka Consumer


from kafka import KafkaConsumer
import json

# Configuration for Kafka broker
KAFKA_BROKER = 'localhost:9092'
TOPIC_NAME = 'my_realtime_events'
GROUP_ID = 'my_consumer_group' # Consumers in the same group share partitions

# Initialize Kafka Consumer
consumer = KafkaConsumer(
    TOPIC_NAME,
    bootstrap_servers=[KAFKA_BROKER],
    auto_offset_reset='earliest', # Start reading from the beginning of the topic
    enable_auto_commit=True,
    group_id=GROUP_ID,
    value_deserializer=lambda x: json.loads(x.decode('utf-8'))
)

print(f"Listening for messages on topic: {TOPIC_NAME} in group: {GROUP_ID}")

try:
    for message in consumer:
        print(f"Received: Topic={message.topic}, Partition={message.partition}, "
              f"Offset={message.offset}, Value={message.value}")
except KeyboardInterrupt:
    print("Consumer stopped.")
finally:
    consumer.close()

These simple examples demonstrate Kafka’s role in reliable data transport. Data produced by one application can be consumed by multiple others, making it an ideal message bus for complex streaming architectures.

Build Real-Time Streaming Pipelines with Kafka & Flink
Generated Image

The Brain: Apache Flink – The State-Enabled Stream Processor

While Kafka excels at moving data, Apache Flink is where the real-time intelligence happens. Flink is a powerful, open-source distributed stream processing framework for stateful computations over unbounded and bounded data streams. It can process data at very high speeds, with low latency, and is designed to handle complex event processing (CEP), real-time analytics, and data-driven applications.

Key Features of Flink

  • Unified Stream and Batch Processing: Flink treats batch processing as a special case of stream processing (bounded streams), offering a single runtime for both paradigms.
  • Stateful Processing: Flink can maintain and manage state over time, which is crucial for operations like aggregations, windowing, and pattern detection. This state is fault-tolerant and can be very large.
  • Event Time Processing: Flink can correctly process events based on their actual generation time, rather than the time they arrive at the processor. This is achieved through Watermarks, which handle out-of-order events.
  • Exactly-Once Semantics: Guarantees that each event affects the computation exactly once, even in the face of failures, crucial for financial transactions or critical analytics.
  • Fault Tolerance and High Availability: Achieved through distributed checkpoints and savepoints, Flink can recover from failures without data loss.
  • High Throughput and Low Latency: Optimized for performance, Flink can process millions of events per second with sub-second latency.
  • Flexible APIs: Offers DataStream API (for low-level control over stream processing), Table API, and SQL API (for declarative processing).

Flink vs. Other Stream Processors (Briefly)

While frameworks like Spark Streaming also offer stream processing capabilities, Flink often distinguishes itself with its true streaming nature and advanced state management. Spark Streaming operates on micro-batches, which introduces inherent latency, whereas Flink processes individual events or small groups of events continuously. Flink’s event-time processing and robust state management for long-running computations are particularly strong for complex real-time applications.

Basic Flink DataStream Program Example (PyFlink)

Let’s look at a simple PyFlink program. We’ll simulate reading a stream of numbers, filtering them, and then printing the result. Ensure you have PyFlink installed (pip install apache-flink) and a Flink cluster running (or run in local mode).


from pyflink.datastream import StreamExecutionEnvironment
from pyflink.datastream.connectors.kafka import FlinkKafkaConsumer, FlinkKafkaProducer
from pyflink.common.serialization import SimpleStringSchema
from pyflink.datastream.functions import FlatMapFunction
from pyflink.common.typeinfo import Types

# Define a simple FlatMap function
class WordSplitter(FlatMapFunction):
    def flat_map(self, sentence):
        for word in sentence.split(" "):
            yield word

def main():
    # 1. Set up the streaming execution environment
    env = StreamExecutionEnvironment.get_execution_environment()
    env.set_parallelism(1) # For simplicity, run with a single task slot

    # 2. Define a data source (e.g., from a collection for demonstration)
    # In a real scenario, this would be a Kafka source
    data_stream = env.from_collection(
        collection=[
            "hello flink stream",
            "hello kafka",
            "flink kafka pipeline"
        ],
        type_info=Types.STRING()
    )

    # 3. Apply transformations
    # Split sentences into words and filter for words containing 'flink'
    processed_stream = data_stream \
        .flat_map(WordSplitter(), output_type=Types.STRING()) \
        .filter(lambda word: "flink" in word.lower())

    # 4. Define a data sink (e.g., print to console)
    # In a real scenario, this would be a Kafka sink or a database
    processed_stream.print()

    # 5. Execute the Flink job
    env.execute("SimplePyFlinkStreamingJob")

if __name__ == '__main__':
    main()

This example demonstrates the basic flow: setting up the environment, defining a source, applying transformations (flat_map, filter), and defining a sink. The real power comes when connecting to external systems like Kafka and performing more complex, stateful operations.

The Synergy: Kafka + Flink Architecture for Real-Time Pipelines

The combination of Apache Kafka and Apache Flink creates a robust, scalable, and highly capable architecture for nearly any real-time data processing need. Kafka provides the distributed, durable, and highly available messaging layer, acting as a buffer and a central hub for all events. Flink, then, consumes these events from Kafka, processes them with rich, stateful logic, and can then write the results back to Kafka, a database, or a dashboard for immediate action or further analysis.

Conceptual Architecture (in Words)

  1. Data Sources: Various applications, IoT devices, web servers, databases, etc., generate events.
  2. Kafka Producers: These producers push events into specific Kafka topics. Each topic acts as a categorized stream of data. Kafka ensures these events are durable and available for multiple consumers.
  3. Kafka Cluster: Stores the incoming event streams reliably across its brokers and partitions.
  4. Flink Application: This is the “brain” of the pipeline. It acts as a Kafka consumer, subscribing to one or more input topics.
  5. Flink Processing: Inside the Flink application, events are deserialized and undergo various transformations:
    • Filtering: Discarding irrelevant events.
    • Mapping/Transforming: Changing event schemas or enriching data.
    • Keying: Grouping events by a specific attribute (e.g., user ID, device ID) for stateful operations.
    • Windowing: Grouping events based on time (e.g., last 5 minutes) or count for aggregations.
    • Stateful Operations: Maintaining state (e.g., running counts, averages, last seen values) for each key or window.
    • Joins: Combining event streams or joining with external static data (e.g., from another Kafka topic or a database lookup).
    • Complex Event Processing (CEP): Detecting sequences of events or specific patterns.
  6. Kafka Sinks / Other Sinks: After processing, Flink can publish the results to new Kafka topics (e.g., a “processed_events” topic, an “alerts” topic), push them into a time-series database (InfluxDB, Prometheus), a traditional RDBMS, a NoSQL database (Cassandra, MongoDB), or directly to a real-time dashboard.

This architecture decouples the ingestion layer from the processing layer, allowing each component to scale independently and ensuring that processing logic can be updated without affecting data ingestion. Kafka’s ability to retain messages also provides a powerful replay mechanism, invaluable for testing, debugging, and backfilling data.

Build Real-Time Streaming Pipelines with Kafka & Flink
Generated Image

Building a Real-Time Streaming Pipeline: Practical Scenario (IoT Device Monitoring)

Let’s solidify our understanding by walking through a common real-world use case: real-time IoT device monitoring and anomaly detection. Imagine a fleet of smart sensors (temperature, pressure, humidity, etc.) sending data continuously. We want to process this data to:

  1. Calculate average sensor readings over a short time window.
  2. Detect immediate anomalies (e.g., a sudden spike in temperature) for individual devices.
  3. Output aggregated metrics for dashboards and critical alerts for immediate action.

Architecture for IoT Monitoring (Detailed)

  1. IoT Devices: Sensors generate JSON messages like {"device_id": "sensor_123", "timestamp": 1678886400, "temperature": 25.5, "humidity": 60}.
  2. Kafka Ingestion Topic (`iot-sensor-data`): A Kafka producer (running on edge devices or a gateway) sends these JSON messages to the iot-sensor-data topic. This topic will have multiple partitions for scalability.
  3. Flink Application (`IoTMonitoringJob`):
    • Consumes messages from iot-sensor-data.
    • Deserializes JSON strings into structured data.
    • KeyBy `device_id`: All events from a specific device are routed to the same Flink task for stateful processing.
    • Windowing: Applies a tumbling time window (e.g., 5 minutes) to calculate average temperature/humidity for each device.
    • Stateful Anomaly Detection: For each device, it maintains the last observed temperature and compares the current reading to detect sudden, significant changes (e.g., a jump of 10 degrees within a second).
    • Event Time Processing: Uses watermarks to handle out-of-order events, ensuring accurate window calculations.
  4. Kafka Output Topics:
    • iot-aggregated-metrics: Flink publishes the 5-minute average readings here. Consumers could include a time-series database for historical trending or a dashboard application.
    • iot-anomalies: Flink publishes immediate anomaly alerts here. Consumers could include an alert system (email, SMS, PagerDuty) or a real-time dashboard highlighting critical events.

Implementation Steps with PyFlink

We’ll create a PyFlink application that connects to Kafka, performs the described processing, and writes results back to Kafka. You’ll need:

  • A running Kafka cluster (e.g., Docker: docker-compose up with a Kafka setup).
  • PyFlink installed (pip install apache-flink).
  • kafka-python for a test producer (pip install kafka-python).

1. Setup Kafka Topics

Ensure you have the input and output topics created. You can do this via Kafka’s command-line tools or by having Flink or producers auto-create them (though explicit creation is often better for production).


# Assuming kafka-topics.sh is in your Kafka bin directory
# Create input topic
kafka-topics.sh --bootstrap-server localhost:9092 --create --topic iot-sensor-data --partitions 3 --replication-factor 1

# Create output topics
kafka-topics.sh --bootstrap-server localhost:9092 --create --topic iot-aggregated-metrics --partitions 1 --replication-factor 1
kafka-topics.sh --bootstrap-server localhost:9092 --create --topic iot-anomalies --partitions 1 --replication-factor 1

2. Simulate IoT Device Data (Python Producer)

This producer will send realistic sensor data to iot-sensor-data.


# iot_producer.py
from kafka import KafkaProducer
import json
import time
import random

KAFKA_BROKER = 'localhost:9092'
TOPIC_NAME = 'iot-sensor-data'
DEVICE_IDS = ['sensor_A', 'sensor_B', 'sensor_C']

producer = KafkaProducer(
    bootstrap_servers=[KAFKA_BROKER],
    value_serializer=lambda v: json.dumps(v).encode('utf-8')
)

print(f"Starting IoT sensor data producer for topic: {TOPIC_NAME}")

try:
    while True:
        device_id = random.choice(DEVICE_IDS)
        temperature = round(random.uniform(20.0, 30.0), 2)
        humidity = round(random.uniform(50.0, 70.0), 2)

        # Introduce some anomalies occasionally
        if random.random() < 0.05: # 5% chance of anomaly
            temperature += random.uniform(10.0, 20.0) # Sudden temperature spike
            print(f"!!! ANOMALY INJECTED for {device_id}: Temp={temperature} !!!")

        event = {
            'device_id': device_id,
            'timestamp': int(time.time() * 1000), # Milliseconds since epoch
            'temperature': temperature,
            'humidity': humidity
        }
        producer.send(TOPIC_NAME, value=event)
        # print(f"Sent: {event}")
        time.sleep(random.uniform(0.1, 0.5)) # Send data frequently
except KeyboardInterrupt:
    print("Producer stopped.")
finally:
    producer.flush()
    producer.close()

3. Flink Application for IoT Monitoring (`iot_flink_job.py`)

This is the core Flink job. It will include deserialization, event-time watermarking, windowing, and a stateful process function for anomaly detection.


# iot_flink_job.py
import json
from pyflink.datastream import StreamExecutionEnvironment, TimeCharacteristic
from pyflink.datastream.connectors.kafka import FlinkKafkaConsumer, FlinkKafkaProducer
from pyflink.common.serialization import SimpleStringSchema, DeserializationSchema, SerializationSchema
from pyflink.common.typeinfo import Types
from pyflink.datastream.functions import ProcessWindowFunction, KeyedProcessFunction
from pyflink.datastream.window import TumblingEventTimeWindows
from pyflink.datastream.time_characteristic import Time
from pyflink.datastream.checkpointing_mode import CheckpointingMode
from pyflink.util.java_utils import get_gateway

# --- Configuration ---
KAFKA_BROKER = 'localhost:9092'
INPUT_TOPIC = 'iot-sensor-data'
AGGREGATED_OUTPUT_TOPIC = 'iot-aggregated-metrics'
ANOMALY_OUTPUT_TOPIC = 'iot-anomalies'
FLINK_JOB_NAME = 'IoTRealTimeMonitoring'

# --- Custom Deserialization Schema for Sensor Data ---
class SensorDataDeserializationSchema(DeserializationSchema):
def deserialize(self, message):
return json.loads(message.decode('utf-8'))

def is_end_of_stream(self, message):
return False

def get_produced_type(self):
# Define the structure of the deserialized data
return Types.ROW([
Types.STRING(), # device_id
Types.LONG(), # timestamp
Types.DOUBLE(), # temperature
Types.DOUBLE() # humidity
], ['device_id', 'timestamp', 'temperature', 'humidity'])

# --- Custom Serialization Schema for Output ---
class JsonSerializationSchema(SerializationSchema):
def serialize(self, element):
return json.dumps(element).encode('utf-8')

# --- Window Function for Aggregated Metrics (e.g., average temperature) ---
class AverageTemperatureWindowFunction(ProcessWindowFunction):
def process(self, key, context, elements):
device_id = key
temperatures = [e[2] for e in elements] # e[2] is temperature
if not temperatures:
return

avg_temp = sum(temperatures) / len(temperatures)
window_start = context.window().start
window_end = context.window().end

yield {
'device_id': device_id,
'window_start': window_start,
'window_end': window_end,
'average_temperature': round(avg_temp, 2),
'num_readings': len(temperatures)
}

def get_result_type(self):
return Types.ROW([
Types.STRING(),
Types.LONG(),
Types.LONG(),
Types.DOUBLE(),
Types.INT()
], ['device_id', 'window_start', 'window_end', 'average_temperature', 'num_readings'])

# --- Keyed Process Function for Real-time Anomaly Detection ---
class AnomalyDetector(KeyedProcessFunction):
def open(self, runtime_context):
# Define a ValueState to store the last known temperature
self.last_temperature_state = runtime_context.get_state(
"last_temperature", Types.DOUBLE()
)
self.temperature_threshold = 10.0 # Degrees C change to consider an anomaly

def process_element(self, value, ctx: 'KeyedProcessFunction.Context'):
device_id, timestamp, current_temp, humidity = value

last_temp = self.last_temperature_state.value()

if last_temp is not None:
# Check for sudden temperature change
if abs(current_temp - last_temp) > self.temperature_threshold:
anomaly_message = {
'device_id': device_id,
'timestamp': timestamp,
'current_temperature': current_temp,
'last_temperature': last_temp,
'anomaly_type': 'SuddenTemperatureSpike',
'message': f"Temperature changed from {last_temp} to {current_temp} - ANOMALY!"
}
yield anomaly_message # Emit anomaly

# Update last_temperature_state for the next event for this device
self.last_temperature_state.update(current_temp)

def get_result_type(self):
return Types.ROW([
Types.STRING(),
Types.LONG(),
Types.DOUBLE(),
Types.DOUBLE(),
Types.STRING(),
Types.STRING()
], ['device_id', 'timestamp', 'current_temperature', 'last_temperature', 'anomaly_type', 'message'])

def main():
# 1. Set up the streaming execution environment
env = StreamExecutionEnvironment.get_execution_environment()
env.set_parallelism(1) # Use more in production
env.set_stream_time_characteristic(TimeCharacteristic.EventTime)

# Configure checkpointing for fault tolerance (recommended for production)
env.enable_checkpointing(5000) # Checkpoint every 5 seconds
env.get_checkpoint_config().set_checkpointing_mode(CheckpointingMode.EXACTLY_ONCE)
env.get_checkpoint_config().set_checkpoint_timeout(60000) # 1 minute timeout
env.get_checkpoint_config().set_min_pause_between_checkpoints(5000) # Min 5s pause

# 2. Configure Kafka Source
kafka_consumer = FlinkKafkaConsumer(
topics=INPUT_TOPIC,
deserialization_schema=SensorDataDeserializationSchema(),
properties={
'bootstrap.servers': KAFKA_BROKER,
'group.id': FLINK_JOB_NAME + '_consumer_group',
'auto.offset.reset': 'latest' # Start from latest messages
}
)

# Assign timestamps and watermarks for event-time processing
# We're assuming events are largely in order and using a bounded out-of-orderness watermark
# The timestamp is the 'timestamp' field in our sensor data (long in ms)
sensor_data_stream = env.add_source(kafka_consumer) \
.assign_timestamps_and_watermarks(
# Using a lambda for timestamp extraction, then a BoundedOutOfOrdernessTimestampExtractor
# This is a common pattern for event time processing
# Here we just use the existing timestamp and assume some small out-of-orderness
# For simplicity in PyFlink, direct use of `with_timestamp_assigner` is often easier
# but requires a Java TimestampAssigner for full flexibility.
# For this example, we'll manually assign and use a simple fixed delay watermark strategy.
# In a real PyFlink setup, you might use more advanced timestamp extractors or custom ones.
# For now, let's keep it simple

Written by

Khader Vali

Senior Software Engineer specializing in cloud architecture, real-time systems, and enterprise-scale applications.

Share this article

Related Articles

Distributed Caching Strategies: Redis, Memcached, and CDN Patterns

Jul 17, 2026 · 17 min read

Building Real-Time Streaming Pipelines with Kafka & Flink

Jul 10, 2026 · 7 min read

Build Real-Time Pipelines: Kafka & Flink Guide

Jun 27, 2026 · 1 min read