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 stream processing. Master the synergy of these tools.

Khader Vali July 27, 2026 16 min read

Building Real-Time Streaming Pipelines with Apache Kafka and Flink

In today’s data-driven world, the ability to process and react to information in real-time is no longer a luxury but a necessity. From fraud detection to personalized recommendations, from IoT monitoring to financial trading, businesses demand immediate insights from their ever-flowing data streams. Batch processing, while still valuable for certain use cases, often falls short when milliseconds matter.

Enter the formidable duo: Apache Kafka and Apache Flink. Together, they form the backbone of many modern, high-throughput, low-latency streaming data architectures. Kafka provides the scalable, durable, and fault-tolerant messaging layer that ingests and stores your continuous data streams, while Flink offers the powerful, stateful, and event-time aware processing engine that transforms, aggregates, and analyzes that data in real-time.

As a senior engineer at Khadervali.com, I’ve had the opportunity to architect and implement numerous real-time systems. In this comprehensive guide, I’ll demystify the process, walking you through the core concepts, practical implementation, and best practices for building robust real-time streaming pipelines with Kafka and Flink. By the end, you’ll have a clear understanding of how to leverage these technologies to unlock the true potential of your streaming data.

The Real-Time Imperative: Why Now More Than Ever?

The shift towards real-time processing is driven by several critical business needs:

  • Immediate Insights: Gaining instant knowledge from events as they occur allows for quicker decision-making and proactive measures.
  • Enhanced User Experience: Personalizing content, offering real-time recommendations, or detecting and resolving issues as they happen significantly improves user satisfaction.
  • Fraud Detection: Identifying anomalous patterns in financial transactions or online activities in real-time can prevent significant losses.
  • IoT Monitoring: Tracking sensor data from thousands or millions of devices to detect equipment failures, optimize operations, or trigger alerts as soon as thresholds are crossed.
  • Operational Efficiency: Monitoring system health, log analysis, and performance metrics in real-time to identify bottlenecks and ensure smooth operations.
  • Competitive Advantage: Businesses that can react faster to market changes, customer behavior, or emerging threats often gain a significant edge.

However, real-time processing comes with its own set of challenges: managing continuous data streams, handling out-of-order events, maintaining state across potentially unbounded data, ensuring fault tolerance, and guaranteeing data consistency (like exactly-once semantics). This is precisely where Kafka and Flink shine.

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

Apache Kafka: The Unifying Data Backbone

Apache Kafka is a distributed streaming platform designed to handle high-throughput, fault-tolerant, and scalable real-time data feeds. It acts as a central nervous system for your data, decoupling producers from consumers and providing a durable log of events.

Key Kafka Concepts

  • Topics: A category or feed name to which records are published. Topics are logically partitioned.
  • Producers: Applications that publish records to Kafka topics.
  • Consumers: Applications that subscribe to topics and process the records published to them.
  • Brokers: Kafka servers that store data. A Kafka cluster consists of multiple brokers.
  • Partitions: Topics are divided into partitions. Each partition is an ordered, immutable sequence of records. Records are appended to the end of a partition.
  • Offsets: Each record within a partition has a unique, sequential ID called an offset. Consumers use offsets to track their position in a partition.
  • Consumer Groups: A group of consumers that collectively consume records from one or more topics. Each partition is consumed by exactly one consumer within a group, enabling parallel processing.
  • Replication: Partitions can be replicated across multiple brokers to provide fault tolerance.

Why Kafka for Real-Time Pipelines?

  • Durability: Data written to Kafka is persisted on disk and replicated, making it highly durable even in case of broker failures.
  • Scalability: Kafka can scale horizontally to handle massive volumes of data and high numbers of producers and consumers.
  • High Throughput: Designed for high-performance, it can handle millions of messages per second.
  • Fault Tolerance: With replication and leader election, Kafka ensures data availability and system resilience.
  • Decoupling: Producers and consumers are independent, allowing different applications to evolve at their own pace without impacting each other.

Example: Kafka Producer (Python)

Let’s look at a simple Python producer that generates some simulated sensor data and sends it to a Kafka topic.


import json
import time
import random
from kafka import KafkaProducer

# Configuration
KAFKA_BROKER = 'localhost:9092' # Replace with your Kafka broker address
KAFKA_TOPIC = 'sensor_data'

def serialize_json(obj):
    return json.dumps(obj).encode('utf-8')

producer = KafkaProducer(
    bootstrap_servers=[KAFKA_BROKER],
    value_serializer=serialize_json
)

print(f"Starting Kafka producer for topic: {KAFKA_TOPIC}")

try:
    device_ids = ['device_001', 'device_002', 'device_003', 'device_004']
    
    while True:
        device_id = random.choice(device_ids)
        temperature = round(random.uniform(20.0, 35.0), 2) # Simulate temperature
        humidity = round(random.uniform(40.0, 60.0), 2)    # Simulate humidity
        timestamp = int(time.time() * 1000) # Milliseconds timestamp

        data = {
            'device_id': device_id,
            'timestamp': timestamp,
            'temperature': temperature,
            'humidity': humidity
        }
        
        producer.send(KAFKA_TOPIC, value=data)
        print(f"Produced: {data}")
        time.sleep(random.uniform(0.1, 1.0)) # Send data every 0.1 to 1 second

except KeyboardInterrupt:
    print("Producer stopped.")
finally:
    producer.close()

This script connects to a Kafka broker, serializes Python dictionaries into JSON, and sends them to the sensor_data topic. This will be our input stream for Flink.

Apache Flink: The Stream Processing Powerhouse

Apache Flink is a powerful open-source stream processing framework designed for high-performance, low-latency, and fault-tolerant computations on unbounded and bounded data streams. What sets Flink apart is its ability to handle complex stateful computations over event time, making it ideal for sophisticated real-time analytics.

Key Flink Concepts and Features

  • Unified Stream and Batch Processing: Flink’s core is a stream processing engine. Batch jobs are treated as finite streams, simplifying development and deployment across different data processing paradigms.
  • DataStream API: The primary API for building streaming applications, allowing transformations on continuous streams of data.
  • State Management: Flink provides robust and fault-tolerant state management. Applications can maintain state (e.g., counts, sums, aggregations) across events, even in the face of failures, crucial for complex stream processing.
  • Event Time Processing: Flink supports event time (the time an event occurred at its source) as opposed to processing time (the time an event is processed). This allows for correct results even with out-of-order or late-arriving data through the use of Watermarks.
  • Fault Tolerance & Exactly-Once Semantics: Flink achieves fault tolerance through distributed checkpoints. It can recover from failures by restoring the application’s state from the latest successful checkpoint, guaranteeing exactly-once processing for many connectors.
  • Windowing: Essential for stream processing, Flink supports various types of windows (tumbling, sliding, session) to group events based on time or count for aggregations.
  • Low Latency: Designed for sub-second latency, making it suitable for real-time applications.
  • Connectors: Flink provides connectors for various systems like Kafka, HDFS, Cassandra, Elasticsearch, and more, allowing easy integration with your data ecosystem.

Flink’s Architecture (in words)

A Flink cluster typically consists of two types of daemon processes:

  1. JobManager: The master node. It coordinates the execution of Flink applications, schedules tasks, manages checkpoints, and handles fault recovery. There can be multiple JobManagers for high availability.
  2. TaskManagers: The worker nodes. They execute the actual tasks (operators) of a Flink job, buffer data, and exchange data streams. Each TaskManager has a certain number of slots, which are units of execution parallelism.

When you submit a Flink application, the JobManager receives the job graph, optimizes it, and distributes the tasks to available TaskManager slots. Data flows between tasks in a streaming fashion.

Architecting the Real-Time Pipeline: Kafka & Flink in Harmony

The synergy between Kafka and Flink is powerful. Kafka acts as a durable, highly available message bus, while Flink provides the computational muscle. Let’s describe a typical architecture:

Conceptual Architecture Flow:

[Data Sources] --> [Kafka Topics (Raw Data Ingestion)] --> [Flink Application (Processing Logic)] --> [Kafka Topics (Processed Data/Alerts)] --> [Data Sinks (Databases/Dashboards/APIs)]

Let’s break down this flow:

  1. Data Ingestion (Kafka Producers):

    • Various data sources (IoT devices, web applications, log files, databases via CDC tools like Debezium) send their raw events to specific Kafka topics.
    • Kafka ensures these events are durably stored, ordered within partitions, and available for multiple consumers.
  2. Stream Processing (Flink Application):

    • A Flink application acts as a Kafka consumer, reading events from one or more input topics.
    • Inside Flink, the data undergoes transformations: parsing, filtering, enrichment (e.g., joining with static reference data), aggregations (e.g., calculating sums, averages over time windows), stateful computations (e.g., session tracking, anomaly detection).
    • Flink’s event-time processing and state management ensure correct and consistent results, even with complex logic and out-of-order data.
  3. Output & Persistence (Kafka Producers / Other Sinks):

    • After processing, Flink can write the results to various sinks:
      • Back to Kafka: For further processing by other Flink jobs, downstream microservices, or historical storage. This creates a data-driven architecture where processed data becomes a new source of truth.
      • Databases: To NoSQL stores (e.g., Cassandra, MongoDB) for quick lookups, or relational databases for structured storage.
      • Data Warehouses/Lakes: For analytical queries and long-term storage (e.g., HDFS, S3).
      • Monitoring Systems/Dashboards: Directly update dashboards (e.g., via web sockets, Elasticsearch) for real-time visualization.
      • APIs/Alerting Systems: Trigger alerts, send notifications, or update other services.

This architecture is highly scalable, resilient, and flexible. Kafka provides the reliable messaging, while Flink provides the sophisticated processing capabilities, allowing you to build complex real-time applications with confidence.

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

Building a Practical Real-Time Pipeline: IoT Sensor Analytics

Let’s put theory into practice with a common real-world scenario: real-time monitoring and anomaly detection for IoT sensor data. We’ll simulate sensor readings, push them to Kafka, and then use Flink to process them to detect high-temperature alerts.

Scenario Details:

  • Input Data: Simulated sensor readings (device ID, timestamp, temperature, humidity) in JSON format.
  • Goal: Detect if any device’s temperature exceeds a threshold (e.g., 30°C) and generate an alert. Also, calculate the average temperature per device over a 10-second sliding window.
  • Output: Alerts and aggregated statistics written to separate Kafka topics.

Step 1: Kafka Setup (Conceptual)

You’ll need a running Kafka cluster. For local development, Docker Compose is excellent. Assuming Kafka is running, we’ll need two topics:

  • sensor_data: For raw input.
  • temperature_alerts: For high-temperature alerts from Flink.
  • device_avg_temps: For aggregated average temperatures from Flink.

# Example commands (requires Kafka installed or Docker running)
# Create input topic
kafka-topics --create --topic sensor_data --bootstrap-server localhost:9092 --partitions 3 --replication-factor 1
# Create output topic for alerts
kafka-topics --create --topic temperature_alerts --bootstrap-server localhost:9092 --partitions 1 --replication-factor 1
# Create output topic for average temperatures
kafka-topics --create --topic device_avg_temps --bootstrap-server localhost:9092 --partitions 1 --replication-factor 1

Step 2: Data Producer (as shown earlier)

Run the Python producer script provided in the Kafka section to continuously send data to the sensor_data topic.

Step 3: Flink Application (Scala)

We’ll write a Flink application in Scala. First, set up a Scala project (e.g., with sbt or Maven). Here’s the core Flink application logic:


import org.apache.flink.streaming.api.scala._
import org.apache.flink.streaming.api.windowing.time.Time
import org.apache.flink.streaming.connectors.kafka.{FlinkKafkaConsumer, FlinkKafkaProducer}
import org.apache.flink.api.common.serialization.{SimpleStringSchema, DeserializationSchema}
import org.apache.flink.api.common.typeinfo.TypeInformation
import org.apache.flink.api.java.typeutils.TypeExtractor
import org.apache.flink.shaded.jackson2.com.fasterxml.jackson.databind.ObjectMapper
import org.apache.flink.shaded.jackson2.com.fasterxml.jackson.module.scala.DefaultScalaModule
import org.apache.flink.shaded.jackson2.com.fasterxml.jackson.annotation.JsonFormat
import java.util.Properties

// Define the sensor data case class
case class SensorReading(
    device_id: String,
    @JsonFormat(shape = JsonFormat.Shape.NUMBER_INT) // For JSON parsing of long timestamp
    timestamp: Long,
    temperature: Double,
    humidity: Double
)

// Custom DeserializationSchema for SensorReading
class SensorReadingDeserializer extends DeserializationSchema[SensorReading] {
    private val mapper = new ObjectMapper()
    mapper.registerModule(DefaultScalaModule)

    override def deserialize(message: Array[Byte]): SensorReading = {
        mapper.readValue(message, classOf[SensorReading])
    }

    override def isEndOfStream(nextElement: SensorReading): Boolean = false

    override def getProducedType: TypeInformation[SensorReading] =
        TypeExtractor.getForClass(classOf[SensorReading])
}

object SensorAnalyticsJob {
    def main(args: Array[String]): Unit = {
        // 1. Set up the streaming execution environment
        val env = StreamExecutionEnvironment.getExecutionEnvironment
        env.setParallelism(1) // For local testing, set parallelism to 1
        env.enableCheckpointing(5000) // Enable checkpointing every 5 seconds

        // Kafka consumer properties
        val kafkaProps = new Properties()
        kafkaProps.setProperty("bootstrap.servers", "localhost:9092") // Kafka broker address
        kafkaProps.setProperty("group.id", "flink_sensor_analytics")
        kafkaProps.setProperty("auto.offset.reset", "latest") // Start reading from the latest offset

        // 2. Create a Kafka source
        val kafkaConsumer = new FlinkKafkaConsumer[SensorReading](
            "sensor_data", // Input topic
            new SensorReadingDeserializer(),
            kafkaProps
        )
        // Add a watermark strategy for event time processing
        val sensorStream: DataStream[SensorReading] = env.addSource(kafkaConsumer)
            .assignTimestampsAndWatermarks(
                // Use BoundedOutOfOrdernessTimestampExtractor for scenarios where events might arrive a bit out of order
                // with a maximum lateness of 3 seconds.
                org.apache.flink.streaming.api.functions.timestamps.BoundedOutOfOrdernessTimestampExtractor[SensorReading](Time.seconds(3)) {
                    element => element.timestamp // Use the timestamp field from SensorReading
                }
            )

        // --- Stream 1: High Temperature Alerts ---
        // Filter for high temperature readings
        val highTempAlerts: DataStream[String] = sensorStream
            .filter(reading => reading.temperature > 30.0)
            .map(reading => s"ALERT: Device ${reading.device_id} reported high temperature: ${reading.temperature}°C at ${new java.util.Date(reading.timestamp)}")

        // Print alerts to console (for demonstration)
        highTempAlerts.print("High Temp Alert")

        // Sink alerts to a Kafka topic
        val alertProducer = new FlinkKafkaProducer[String](
            "temperature_alerts", // Output topic
            new SimpleStringSchema(),
            kafkaProps,
            FlinkKafkaProducer.Semantic.EXACTLY_ONCE // Ensures exactly-once delivery
        )
        highTempAlerts.addSink(alertProducer)

        // --- Stream 2: Average Temperature per Device (Windowed Aggregation) ---
        // Key the stream by device_id
        val keyedStream = sensorStream.keyBy(_.device_id)

        // Apply a sliding window of 10 seconds, sliding every 5 seconds
        val avgTempStream: DataStream[String] = keyedStream
            .window(org.apache.flink.streaming.api.windowing.windows.TimeWindow.of(Time.seconds(10), Time.seconds(5))) // 10s window, slide every 5s
            .aggregate(
                new org.apache.flink.api.common.functions.AggregateFunction[SensorReading, (Double, Long), String] {
                    override def createAccumulator(): (Double, Long) = (0.0, 0L) // (sum_temp, count)

                    override def add(value: SensorReading, accumulator: (Double, Long)): (Double, Long) =
                        (accumulator._1 + value.temperature, accumulator._2 + 1)

                    override def getResult(accumulator: (Double, Long)): String = {
                        val avgTemp = if (accumulator._2 > 0) accumulator._1 / accumulator._2 else 0.0
                        s"AVG TEMP: Device ${accumulator._2} - Average temperature: ${"%.2f".format(avgTemp)}°C" // Note: device ID logic needs refinement here for multiple devices in one window, will just take a placeholder
                    }

                    override def merge(a: (Double, Long), b: (Double, Long)): (Double, Long) =
                        (a._1 + b._1, a._2 + b._2)
                }
            )
            .map { case (avgTempString) => // Corrected map to extract the string result from aggregation
                // The previous aggregate function returns a String directly, so this map is simplified
                avgTempString
            }


        // Corrected aggregation to output device_id with average temp
        val avgTempStreamCorrected: DataStream[String] = keyedStream
            .window(org.apache.flink.streaming.api.windowing.windows.TimeWindow.of(Time.seconds(10), Time.seconds(5)))
            .aggregate(new org.apache.flink.api.common.functions.AggregateFunction[SensorReading, (String, Double, Long), String] {
                override def createAccumulator(): (String, Double, Long) = ("", 0.0, 0L)

                override def add(value: SensorReading, accumulator: (String, Double, Long)): (String, Double, Long) = {
                    val deviceId = if (accumulator._1.isEmpty) value.device_id else accumulator._1
                    (deviceId, accumulator._2 + value.temperature, accumulator._3 + 1)
                }

                override def getResult(accumulator: (String, Double, Long)): String = {
                    val avgTemp = if (accumulator._3 > 0) accumulator._2 / accumulator._3 else 0.0
                    s"AVG TEMP: Device ${accumulator._1} - Average temperature: ${"%.2f".format(avgTemp)}°C"
                }

                override def merge(a: (String, Double, Long), b: (String, Double, Long)): (String, Double, Long) = {
                    (a._1, a._2 + b._2, a._3 + b._3) // Device ID is consistent per keyed stream
                }
            })

        // Print average temps to console
        avgTempStreamCorrected.print("Avg Temp")

        // Sink average temps to a Kafka topic
        val avgTempProducer = new FlinkKafkaProducer[String](
            "device_avg_temps", // Output topic
            new SimpleStringSchema(),
            kafkaProps,
            FlinkKafkaProducer.Semantic.EXACTLY_ONCE
        )
        avgTempStreamCorrected.addSink(avgTempProducer)

        // 4. Execute the Flink job
        env.execute("Sensor Analytics Job")
    }
}

Explanation of the Flink Code:

  • Environment Setup: We get a StreamExecutionEnvironment and enable checkpointing for fault tolerance.
  • Kafka Source: We use FlinkKafkaConsumer to read JSON messages from the sensor_data topic. A custom SensorReadingDeserializer is used to parse the JSON into our Scala case class SensorReading.
  • Event Time & Watermarks: We assign timestamps and watermarks. This is crucial for correct windowing, especially when data might arrive out of order. We use a BoundedOutOfOrdernessTimestampExtractor, allowing for a 3-second delay in event arrival.
  • High Temperature Alerts Stream:
    • filter: Keeps only readings where the temperature is above 30.0°C.
    • map: Formats the alert message.
    • print: Outputs to the Flink job manager logs (useful for debugging).
    • addSink: Sends the alert messages to the temperature_alerts Kafka topic using FlinkKafkaProducer with EXACTLY_ONCE semantics.
  • Average Temperature Stream:
    • keyBy(_.device_id): Partitions the stream by device_id, ensuring that all readings for a specific device go to the same processing instance.
    • window(Time.seconds(10), Time.seconds(5)): Defines a sliding window. A window of 10 seconds means it considers data from the last 10 seconds. It slides every 5 seconds, meaning new windows start every 5 seconds, overlapping by 5 seconds.
    • aggregate: Applies a custom aggregation function to each window. It calculates the sum of temperatures and the count of readings, then computes the average.
    • addSink: Writes the average temperature strings to the device_avg_temps Kafka topic.

Step 4: Kafka Consumer (for results)

To see the Flink job’s output, you can run simple Kafka consumers for the output topics:


# Consume high temperature alerts
kafka-console-consumer --topic temperature_alerts --bootstrap-server localhost:9092 --from-beginning

# Consume average temperature readings
kafka-console-consumer --topic device_avg_temps --bootstrap-server localhost:9092 --from-beginning

When you run the Python producer and the Flink job, you’ll see alerts appearing in the temperature_alerts topic when temperatures exceed the threshold, and average temperature calculations appearing in the device_avg_temps topic every 5 seconds for each device.

Advanced Concepts and Best Practices

Building production-grade real-time pipelines requires attention to detail and understanding of advanced concepts.

Fault Tolerance and Exactly-Once Semantics

Flink achieves fault tolerance through distributed, asynchronous checkpoints. When a Flink application takes a checkpoint, it records the current state of all operators and the exact position (Kafka offsets) in the input streams. If a TaskManager fails, Flink can restore the entire application’s state from the last successful checkpoint, resuming processing from the exact Kafka offsets, thus guaranteeing exactly-once processing (when combined with idempotent sinks or transactional sinks like the Flink Kafka Producer with EXACTLY_ONCE semantic).

  • Checkpointing Strategy: Configure checkpointing intervals (e.g., every 5-10 seconds), minimum time between checkpoints, and checkpoint storage (e.g., HDFS, S3).
  • Savepoints: Manual snapshots of your application’s state, used for planned upgrades, migrations, or A/B testing.

State Management and Backends

Flink provides several state backends to manage operator state:

  • MemoryStateBackend: Stores state in the JobManager’s memory. Suitable for small state, local development.
  • FsStateBackend: Stores state on the TaskManager’s heap and checkpoints it to a file system (HDFS, S3). Good for larger states.
  • RocksDBStateBackend: Stores state in RocksDB (an embedded key-value store) on the TaskManager’s local disk. This is the most robust and scalable choice for large state, enabling state sizes to exceed available RAM and providing incremental checkpoints.

For most production deployments with significant state, RocksDBStateBackend is the recommended choice.

Event Time vs. Processing Time vs. Ingestion Time

  • Event Time: The time an event occurred at its source. Ideal for correct and deterministic results, especially with out-of-order data. Requires Watermarks.
  • Processing Time: The time an event is processed by an operator. Simplest but prone to inaccuracies due to network latencies, queueing, and backpressure.
  • Ingestion Time: The
Written by

Khader Vali

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

Share this article

Related Articles

API Gateway Patterns: Routing, Auth, Rate Limiting

Jul 12, 2026 · 14 min read

The Fallacy of Zero-Trust Networks Without Identity Verification

Oct 12, 2024 · 1 min read

Database Sharding Strategies for High-Scale Applications

Jul 02, 2026 · 11 min read