System Design

Build Real-Time Streaming Pipelines with Kafka & Flink

Master building real-time streaming pipelines using Apache Kafka for data ingestion and Apache Flink for powerful, stateful stream processing. Learn architecture, code, and best practices.

Khader Vali September 27, 2026 15 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 fundamental necessity. From detecting fraudulent transactions the moment they occur to providing instant personalized recommendations or monitoring critical infrastructure, enterprises across every industry are grappling with an ever-increasing deluge of data that demands immediate attention. Traditional batch processing, while still valuable for certain use cases, often falls short when insights are required within milliseconds or seconds.

This is where real-time streaming pipelines come into play. They enable organizations to ingest, process, analyze, and act upon data as it is generated, unlocking unprecedented agility and competitive advantage. At the heart of many sophisticated real-time architectures lie two powerful open-source technologies: Apache Kafka and Apache Flink.

Apache Kafka provides a robust, scalable, and fault-tolerant platform for publishing and subscribing to streams of records, essentially acting as the central nervous system for your data. Apache Flink, on the other hand, is a stateful stream processing framework capable of performing complex computations on these data streams with high throughput and low latency, making it the brain that interprets and acts upon the information.

Together, Kafka and Flink form an incredibly potent combination for constructing end-to-end real-time streaming pipelines that are not only highly performant but also resilient and scalable. As a senior software engineer who has navigated the complexities of building such systems, I’m here to guide you through the architecture, core concepts, practical implementation, and best practices of leveraging these technologies.

This article will delve deep into:

  • The fundamental roles and benefits of Apache Kafka in a streaming ecosystem.
  • The advanced capabilities of Apache Flink for real-time stream processing.
  • How to architect and integrate Kafka and Flink into a cohesive pipeline.
  • A detailed, step-by-step example of building a real-world streaming application with code.
  • Advanced concepts and best practices for building robust and scalable systems.

Let’s embark on this journey to unlock the full potential of real-time data.

The Foundation: Apache Kafka

Before we dive into processing, we need a reliable way to get our data from its source to its destination. This is where Apache Kafka shines. Originally developed at LinkedIn, Kafka is a distributed streaming platform that enables you to publish, subscribe to, store, and process streams of records in a fault-tolerant way.

What is Apache Kafka?

At its core, Kafka is a distributed commit log. It stores records (messages) in topics, which are categorized feeds of data. Producers write data to topics, and consumers read data from topics. The beauty of Kafka lies in its ability to handle high volumes of data with low latency, making it an ideal choice for event sourcing, message queuing, and log aggregation.

Kafka Core Concepts

  • Topics: A category or feed name to which records are published. Topics are partitioned, meaning they are divided into multiple segments.
  • Partitions: Topics are split into partitions. Each partition is an ordered, immutable sequence of records that is continually appended to. Records in a partition are assigned a sequential ID number called an “offset.”
  • Producers: Applications that publish records to Kafka topics. Producers decide which partition to write to (e.g., round-robin or based on a key).
  • Consumers: Applications that subscribe to one or more topics and process the stream of records produced to them. Consumers read data from partitions.
  • Brokers: A Kafka cluster consists of one or more servers (Kafka brokers). Each broker stores some partitions of topics.
  • Consumer Groups: A group of consumers that collectively consume from one or more topics. Each partition is consumed by exactly one consumer within a consumer group, allowing for parallel processing and load balancing.
  • Zookeeper (or Kraft): Historically, Kafka used Apache ZooKeeper for managing and coordinating brokers. Newer versions (2.8+) offer KRaft mode, which removes the ZooKeeper dependency, simplifying deployment and improving scalability. For this article, we’ll assume a general Kafka setup, but be aware of this evolution.

Why Kafka for Streaming Pipelines?

Kafka provides several critical advantages that make it an indispensable component of real-time streaming architectures:

  • High Throughput: Capable of handling millions of messages per second.
  • Scalability: Easily scales horizontally by adding more brokers and partitions.
  • Durability: Records are persisted on disk and replicated across multiple brokers, ensuring data is not lost even if a broker fails.
  • Fault Tolerance: The distributed nature and replication provide resilience against failures.
  • Decoupling: Producers and consumers are completely decoupled. They don’t know about each other, allowing independent development and scaling.
  • Backpressure Handling: Consumers can read at their own pace, meaning a slow consumer won’t impact producers or other consumers.
  • Ordered Guarantees: Records within a single partition are always ordered, which is crucial for many stateful applications.

Simple Kafka Producer Example (Java)

To illustrate, here’s a basic Java producer that sends messages to a Kafka topic. In a real application, this producer would be integrated into your data source application (e.g., an IoT gateway, a web server, a database change data capture tool).

import org.apache.kafka.clients.producer.*;
import org.apache.kafka.common.serialization.StringSerializer;

import java.util.Properties;

public class SimpleKafkaProducer {

    private static final String TOPIC = "sensor-raw-data";
    private static final String BOOTSTRAP_SERVERS = "localhost:9092"; // Or your Kafka cluster

    public static void main(String[] args) {
        Properties props = new Properties();
        props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, BOOTSTRAP_SERVERS);
        props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
        props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());

        Producer<String, String> producer = new KafkaProducer<>(props);

        try {
            for (int i = 0; i < 10; i++) {
                String key = "sensor-" + (i % 3); // Example key for partitioning
                String value = "{\"sensorId\": \"sensor-" + (i % 3) + "\", \"timestamp\": " + System.currentTimeMillis() + ", \"temperature\": " + (20 + Math.random() * 10) + "}";
                
                ProducerRecord<String, String> record = new ProducerRecord<>(TOPIC, key, value);

                producer.send(record, new Callback() {
                    @Override
                    public void onCompletion(RecordMetadata metadata, Exception exception) {
                        if (exception == null) {
                            System.out.println("Message sent successfully to topic " + metadata.topic() +
                                    ", partition " + metadata.partition() +
                                    ", offset " + metadata.offset());
                        } else {
                            System.err.println("Error sending message: " + exception.getMessage());
                        }
                    }
                });
                Thread.sleep(1000); // Send a message every second
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            producer.close();
        }
    }
}

This producer sends JSON strings representing sensor data to a topic named `sensor-raw-data`. Each message has a key (`sensor-X`) which Kafka can use to ensure messages with the same key go to the same partition, preserving order for that key.

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

With Kafka reliably moving our data, we now need a powerful engine to process it. Enter Apache Flink. Flink is an open-source stream processing framework that offers unparalleled capabilities for stateful computations over unbounded and bounded data streams.

Unlike some other stream processing systems that might treat streams as micro-batches, Flink is a true stream processor. It processes data one event at a time, providing very low latency. It’s designed for continuous, high-volume data streams and is particularly adept at stateful computations, meaning it can remember information about past events to influence future processing.

  • High Throughput and Low Latency: Flink achieves impressive performance by processing events individually and efficiently managing resources.
  • Stateful Processing: This is a game-changer. Flink can maintain and manage state during stream processing. This state is fault-tolerant, checkpointed, and can be very large, enabling complex aggregations, pattern matching, and machine learning models over continuous data.
  • Event-Time Processing and Watermarks: Flink accurately handles out-of-order events using event time (the time an event occurred at its source) and “watermarks.” Watermarks signal that all events up to a certain timestamp have arrived, allowing Flink to correctly process windows and aggregations even if data arrives late or out of order.
  • Windowing: Flink provides powerful windowing mechanisms (tumbling, sliding, session windows) to aggregate events based on time or count, essential for time-series analytics.
  • Fault Tolerance and Exactly-Once Semantics: Through its distributed checkpoints and savepoints, Flink guarantees exactly-once processing semantics for stateful applications, even in the event of failures. This means each event is processed once and only once, preventing data duplication or loss.
  • Flexible API Layers: Flink offers multiple API layers for different use cases:
    • DataStream API: The core API for building powerful, custom stream processing applications in Java or Scala.
    • Table API & SQL: A declarative API that allows users to write queries similar to SQL, making it accessible for data analysts and reducing development time for common operations. It can operate on both batch and streaming tables.
  • Connectors: A rich ecosystem of connectors for various data sources and sinks (Kafka, Kinesis, Cassandra, Elasticsearch, relational databases, etc.).

When combined with Kafka, Flink elevates your real-time capabilities:

  • Advanced Analytics: Perform complex aggregations, joins, pattern detection, and machine learning inferences on continuous data streams.
  • Low-Latency Decision Making: Enable immediate responses to events, critical for fraud detection, real-time bidding, or dynamic pricing.
  • Guaranteed Consistency: Achieve exactly-once processing, ensuring data integrity even in complex, stateful transformations.
  • Unified Batch and Stream Processing: Flink’s architecture allows it to process both unbounded (streaming) and bounded (batch) datasets using the same engine and APIs, simplifying your data infrastructure.
  • Scalability and Resilience: Like Kafka, Flink is highly scalable and fault-tolerant, designed to run 24/7 on large clusters.

The synergy between Kafka and Flink is powerful. Kafka acts as the highly available, durable message bus, ingesting raw events and reliably delivering them. Flink consumes these events, applies sophisticated transformations, aggregates state, and then outputs the results back to Kafka or other sinks. This creates a robust, scalable, and modular real-time data flow.

Conceptual Architecture Diagram (in words)

Imagine a flow where data starts from various sources (IoT devices, web applications, databases). These sources don’t directly communicate with the processing engine; instead, they publish their events to designated Kafka topics. This acts as the ingestion layer and a durable buffer.

Here’s a typical flow:

  1. Data Sources:
    • Various applications or devices generate events (e.g., sensor readings, user clicks, financial transactions, logs).
  2. Kafka Producers:
    • Applications acting as Kafka producers send these raw events to specific input Kafka topics.
  3. Kafka Input Topics:
    • Raw, immutable event streams are stored here. This serves as a reliable, durable, and replayable source for Flink.
  4. Flink Application:
    • A Flink job is deployed on a Flink cluster.
    • Kafka Source: The Flink job connects to the Kafka input topics as its data source, consuming events.
    • Transformations & Logic: Flink applies complex, stateful processing logic (e.g., filtering, mapping, windowed aggregations, joins, pattern detection, machine learning models, anomaly detection).
    • Managed State: Flink transparently manages the state of these computations, making it fault-tolerant.
    • Kafka Sink: After processing, Flink publishes the transformed, aggregated, or derived events to one or more output Kafka topics.
  5. Kafka Output Topics:
    • These topics contain the processed, enriched, or aggregated data. They can serve as input for subsequent Flink jobs (creating a pipeline of Flink jobs), or be consumed by other systems.
  6. Downstream Consumers/Sinks:
    • Other applications or services act as Kafka consumers, reading from the output topics.
    • Examples:
      • Dashboards for real-time monitoring (e.g., Grafana, custom UI).
      • Databases for persistence (e.g., Cassandra, Elasticsearch, PostgreSQL).
      • Alerting systems (e.g., PagerDuty, Slack).
      • Machine learning models for further analysis.
      • Other microservices needing real-time updates.

This architecture is highly flexible. You can have multiple Flink jobs, each consuming from different Kafka topics and producing to others, forming a sophisticated event-driven microservices architecture.

Build Real-Time Streaming Pipelines with Kafka & Flink
Generated Image
  • Robustness: Both systems are designed for fault tolerance and high availability.
  • Scalability: Independent scaling of Kafka (by adding brokers/partitions) and Flink (by adding task managers/parallelism).
  • Performance: High-throughput data ingestion and low-latency processing.
  • Data Replayability: Kafka’s durable log allows Flink jobs to restart from a specific point or reprocess historical data easily.
  • Loose Coupling: Kafka acts as an intermediary, decoupling producers from consumers and Flink jobs, enhancing modularity.
  • Exactly-Once Semantics: A critical combination that ensures data integrity throughout the pipeline.

Building a Practical Pipeline: A Step-by-Step Guide

Let’s solidify our understanding by building a concrete example. We’ll simulate an IoT sensor data processing pipeline using Kafka and Flink.

Scenario: Real-Time IoT Sensor Data Processing

Imagine a network of IoT sensors deployed in various environments, continuously sending temperature readings. Our goal is to:

  1. Ingest raw sensor data from Kafka.
  2. Filter out invalid readings (e.g., temperatures outside a reasonable range).
  3. Calculate the average temperature for each sensor over a 10-second window.
  4. Detect temperature anomalies (e.g., a reading significantly higher than the window average).
  5. Sink processed average temperatures to one output Kafka topic.
  6. Sink detected anomalies to another dedicated Kafka topic for alerting.

Data Model

Each sensor reading will be a JSON object with the following structure:


{
    "sensorId": "sensor-1",
    "timestamp": 1678886400000,
    "temperature": 25.5
}

Kafka Setup (Conceptual)

Before running the Flink job, you’d need a running Kafka cluster. For local development, you can use Docker or a single-node setup. We’ll need three topics:

  • sensor-raw: Input topic for raw sensor data.
  • sensor-averages: Output topic for windowed average temperatures.
  • sensor-anomalies: Output topic for detected anomalies.

You can create these topics using Kafka’s command-line tools:


# Assuming Kafka is running and bin is in your PATH
kafka-topics.sh --create --topic sensor-raw --bootstrap-server localhost:9092 --partitions 3 --replication-factor 1
kafka-topics.sh --create --topic sensor-averages --bootstrap-server localhost:9092 --partitions 3 --replication-factor 1
kafka-topics.sh --create --topic sensor-anomalies --bootstrap-server localhost:9092 --partitions 1 --replication-factor 1

Project Setup (Maven)

Create a new Maven project and add the necessary Flink and Kafka connector dependencies to your `pom.xml`:


<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.khadervali</groupId>
    <artifactId>flink-kafka-pipeline</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <flink.version>1.17.1</flink.version> <!-- Use your desired Flink version -->
        <java.version>11</java.version>
        <maven.compiler.source>${java.version}</maven.compiler.source>
        <maven.compiler.target>${java.version}</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <!-- Flink Core -->
        <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-java</artifactId>
            <version>${flink.version}</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-streaming-java</artifactId>
            <version>${flink.version}</version>
            <scope>provided<>/scope>
        </dependency>

        <!-- Flink Kafka Connector -->
        <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-connector-kafka</artifactId>
            <version>${flink.version}</version>
        </dependency>

        <!-- JSON processing -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.15.2</version> <!-- Use a recent version -->
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <!-- Scala compiler plugin for Flink projects with Scala classes -->
            <plugin>
                <groupId>net.alchim31.maven</groupId>
                <artifactId>scala-maven-plugin</artifactId>
                <version>4.5.6</version>
            </plugin>
            <!-- The exec plugin allows to run the example from the command line -->
            <plugin>
                <groupId>org.codehaus.mojo</groupId>
                <artifactId>exec-maven-plugin</artifactId>
                <version>3.1.0</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>exec</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
            <!-- The Flink shade plugin will create a fat jar that includes all dependencies -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-shade-plugin</artifactId>
                <version>3.4.1</version>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>shade</goal>
                        </goals>
                        <configuration>
                            <artifactSet>
                                <excludes>
                                    <!-- Exclude Flink dependencies as they are provided by the cluster -->
                                    <exclude>org.apache.flink:*</exclude>
                                    <exclude>org.scala-lang:*</exclude>
                                    <exclude>org.apache.logging.log4j:*</exclude>
                                </excludes>
                            </artifactSet>
                            <filters>
                                <filter>
                                    <artifact>*:*</artifact>
                                    <excludes>
                                        <exclude>META-INF/*.SF</exclude>
                                        <exclude>META-INF/*.DSA</exclude>
                                        <exclude>META-INF/*.RSA</exclude>
                                    </excludes>
                                </filter>
                            </filters>
                            <transformers>
                                <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                                    <mainClass>com.khadervali.streaming.SensorProcessingJob</mainClass>
                                </transformer>
                            </transformers>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

Code Example 1: Data Model (POJOs)

We’ll create simple POJOs for our sensor data and processed averages. Flink works efficiently with POJOs.

package com.khadervali.streaming.model;

import com.fasterxml.jackson.annotation.JsonProperty;

// Raw Sensor Data
public class SensorReading {
    @JsonProperty("sensorId")
    public String sensorId;
    @JsonProperty("timestamp")
    public long timestamp;
    @JsonProperty("temperature")
    public double temperature;

    public SensorReading() {} // Default constructor for Flink/Jackson

    public SensorReading(String sensorId, long timestamp, double temperature) {
        this.sensorId = sensorId;
        this.timestamp = timestamp;
        this.temperature = temperature;
    }

    // Getters for Flink to access fields (mandatory for POJO type inference)
    public String getSensorId() { return sensorId; }
    public long getTimestamp() { return timestamp; }
    public double getTemperature() { return temperature; }

    // Setters (optional, but good practice for POJOs)
    public void setSensorId(String sensorId) { this.sensorId = sensorId; }
    public void setTimestamp(long timestamp) { this.timestamp = timestamp; }
    public void setTemperature(double temperature) { this.temperature = temperature; }

    @Override
    public String toString() {
        return "SensorReading{" +
               "sensorId='" + sensorId + '\'' +
               ", timestamp=" + timestamp +
               ", temperature=" + temperature +
               '}';
    }
}
package com.khadervali.streaming

Written by

Khader Vali

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

Share this article

Related Articles

Database Sharding Strategies for High-Scale Apps

Aug 19, 2026 · 1 min read

GraphQL Federation: Building Distributed Graphs at Scale

Sep 16, 2026 · 9 min read

Apache Flink Stateful Computations at Scale: A Deep Dive

Jul 11, 2026 · 16 min read