System Design

Building 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.

Khader Vali July 10, 2026 7 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 personalizing user experiences as they interact with an application, real-time insights are critical for competitive advantage and operational efficiency. This is where the powerful combination of Apache Kafka and Apache Flink shines, offering a robust, scalable, and fault-tolerant architecture for building sophisticated real-time streaming pipelines.

As a senior engineer, I’ve seen firsthand how these technologies transform raw, continuous data streams into actionable intelligence. This article will guide you through the intricacies of building such pipelines, detailing the roles of Kafka and Flink, providing architectural insights, hands-on code examples, and best practices that you can apply to your own projects.

Why Real-Time Streaming? The Imperative for Instant Insight

Gone are the days when batch processing, with its inherent latency, was sufficient for all analytical needs. While batch processing still holds its ground for certain historical analyses, the demands of modern applications often require immediate data processing. Consider these scenarios:

  • Fraud Detection: A financial institution needs to identify and block fraudulent transactions within milliseconds to prevent significant losses. Waiting hours for a batch job to run is unacceptable.
  • Personalized Recommendations: E-commerce platforms thrive on recommending products based on a user’s current browsing session and past behavior. Real-time updates to recommendation models are crucial for relevance.
  • IoT Data Analytics: Thousands or millions of sensors continuously generate data – temperature, pressure, location. Analyzing this data in real-time can detect anomalies, predict failures, and optimize operations instantly.
  • Log and Event Monitoring: DevOps teams need immediate alerts on system errors, performance bottlenecks, or security breaches. Real-time processing of logs and application events is vital for proactive incident response.
  • Ride-Sharing/Delivery Services: Matching drivers with riders, optimizing routes, and calculating estimated arrival times all rely on continuously updated geospatial data and real-time processing.

In each of these cases, the value of data diminishes rapidly over time. The ability to ingest, process, and act upon data as it arrives unlocks new possibilities, enhances user experience, and drives business value. This is the realm where real-time streaming architectures excel, and our chosen tools – Kafka and Flink – are at the forefront of this revolution.

<

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

>

To build a robust real-time streaming pipeline, you need two fundamental components: a highly scalable and fault-tolerant messaging system for data ingestion and transport, and a powerful stream processing engine capable of complex computations with strong guarantees. Apache Kafka fills the former role, and Apache Flink excels in the latter.

Apache Kafka: The Distributed Streaming Platform

Apache Kafka is an open-source distributed streaming platform designed for building real-time data pipelines and streaming applications. At its core, Kafka is a distributed commit log, offering high-throughput, low-latency, and fault-tolerant capabilities. It acts as the central nervous system for your streaming data, allowing various applications to publish and subscribe to data streams.

Key Concepts of Kafka:

  • Topics: A category or feed name to which records are published. Topics are logically named channels for data.
  • Producers: Applications that publish (write) records to Kafka topics.
  • Consumers: Applications that subscribe to (read) records from Kafka topics.
  • Brokers: Kafka servers that store data. A Kafka cluster typically consists of multiple brokers to provide fault tolerance and scalability.
  • Partitions: Topics are divided into a set of ordered, immutable sequences of records called partitions. Each record in a partition is assigned a sequential ID number called an offset. Partitions enable parallelism and scalability.
  • Replicas: Each partition can have multiple replicas spread across different brokers. This ensures data durability and fault tolerance; if one broker fails, another replica can take over.
  • Zookeeper: (Historically) Kafka uses Zookeeper for managing and coordinating Kafka brokers, handling leader election, and maintaining metadata about the cluster. Modern Kafka versions are moving towards KRaft mode, which removes the Zookeeper dependency, but many existing setups still rely on it.

Why Kafka for Streaming?

  • Durability: Data written to Kafka is persisted to disk and replicated, ensuring it’s not lost even if brokers fail.
  • Scalability: Kafka scales horizontally. You can add more brokers to increase throughput and storage capacity. Partitions allow for parallel processing by multiple consumers.
  • High Throughput: Designed for millions of messages per second, making it ideal for high-volume data streams.
  • Decoupling: Producers and consumers are decoupled. They don’t need to know about each other, only about the Kafka topic. This allows for independent development and scaling.
  • Backpressure Handling: Kafka acts as a buffer, allowing consumers to process data at their own pace without overwhelming them or losing data.

Kafka’s role in our pipeline is to reliably ingest raw data from various sources, buffer it, and make it available for processing by Flink applications. It acts as a durable, highly available message bus that connects data producers to data consumers.

Apache Flink is a powerful open-source stream processing framework for unbounded and bounded data streams. It’s designed to run stateful computations over data streams at very high speed and with strong consistency guarantees. Flink is often referred to as a “stream-first” processor, meaning it treats everything as a stream, even batch jobs are seen as finite streams.

Key Concepts of Flink:

  • DataStream API: Flink’s primary API for building stream processing applications. It provides primitives for various transformations (map, filter, keyBy, window, join, etc.).
  • Operators: The basic building blocks of a Flink program, representing data transformations (e.g., a map operator applies a function to each element).
  • State Management: Flink allows operators to maintain state across events. This is crucial for complex operations like aggregations (e.g., counting events per key) or pattern matching. Flink provides powerful mechanisms for state fault tolerance (checkpointing).
  • Event Time vs. Processing Time:
    • Event Time: The time when an event occurred at its source. Flink uses watermarks to deal with out-of-order events and correctly process event-time windows.
    • Processing Time: The time when an event is processed by the Flink operator. Simpler but less accurate for temporal operations due to network delays and processing variations.
  • Watermarks: Special timestamped records that flow with the data stream, indicating that all events up to that timestamp are expected to have arrived. They are critical for handling out-of-order events and triggering event-time windows.
  • Checkpoints: Flink periodically takes consistent snapshots of the entire application state and writes them to a durable storage (e.g., HDFS, S3). In case of a failure, Flink can recover the application to the state of the last successful checkpoint, ensuring exactly-once processing semantics.
  • Savepoints: Manually triggered checkpoints that are used for planned operations like upgrading an application, changing parallelism, or A/B testing.

Why Flink for Processing?

  • Exactly-Once Guarantees: Through its robust checkpointing mechanism, Flink can guarantee that each record is processed exactly once, even in the face of failures. This is a crucial feature for financial transactions or critical IoT data.
  • Low Latency: Flink is designed for millisecond-latency processing, making it suitable for real-time analytics and immediate decision-making.
  • Stateful Processing: Its powerful state management allows for complex aggregations, pattern detection, and joins over unbounded streams.
  • High Throughput: Can process millions of events per second with high parallelism.
  • Flexible APIs: Offers DataStream API for low-level control, Table API & SQL for declarative programming, and even Graph API.
  • Scalability: Scales horizontally to handle large volumes of data and complex computations by distributing tasks across a cluster.

Flink’s role is to consume data from Kafka, perform complex transformations, aggregations, and stateful computations, and then often write the results back to Kafka or another sink for further consumption or storage. It’s the “brain” of our real-time pipeline, making sense of the continuous flow of data.

A typical real-time streaming pipeline leveraging Kafka and Flink follows a clear and modular architecture. Let’s visualize this flow:


+-----------------+      +---------------------+      +---------------------+
|   Data Sources  |      |   Kafka Producers   |      |   Kafka Cluster     |
| (IoT Sensors,   |----->| (Ingest data into   |----->| (Topics: raw_data,  |
| Web Servers,    |      |    Kafka topics)    |      |   processed_data)   |
| Databases, etc.)|      +---------------------+      |                     |
+-----------------+                                   |                     |
                                                      |                     |
                                                      +----------^----------+
                                                                 |
                                                                 | (Consumes from raw_data)
                                                                 |
                                                      +----------v----------+
                                                      | Apache Flink        |
                                                      | Application         |
                                                      | (Stream Processing: |
                                                      |  Filtering,       |
                                                      |  Aggregations,    |
                                                      |  Joins, Stateful  |
                                                      |  Computations)    |
                                                      +----------^----------+
                                                                 |
                                                                 | (Writes to processed_data)
                                                                 |
+-----------------+      +---------------------+      +----------v----------+
|  External Sinks |<-----|  Kafka Consumers    |<-----|   Kafka Cluster     |
| (Databases,     |      | (Read processed data |      | (Topics: raw_data,  |
| Dashboards,     |      |   for specific use  |      |   processed_data)   |
| Alerting Sys.)  |      |     cases)          |      |                     |
+-----------------+      +---------------------+      +---------------------+

Let's break down this architectural flow:

  1. Data Sources: This is where your data originates. It could be anything from IoT devices sending sensor readings, web applications emitting user activity logs, transactional databases generating change data capture (CDC) events, or even external APIs.
  2. Kafka Producers: These applications are responsible for connecting to the data sources, collecting the raw data, and publishing it as messages to specific Kafka topics. Each message typically includes a key (for partitioning) and a value (the actual data payload), along with a timestamp. The choice of serialization format (JSON, Avro, Protobuf) is crucial here.
  3. Kafka Cluster (Input Topics): The Kafka cluster acts as a central hub. Producers write to designated "raw data" topics (e.g., sensor_readings_raw, user_clicks_raw). Kafka ensures these messages are durably stored, replicated, and made available to consumers.
  4. Apache Flink Application: This is the heart of your real-time processing. A Flink job is configured to consume messages from one or more input Kafka topics. Within the Flink application, you define a series of operators and transformations that process the incoming stream:

    • Deserialization: Converting raw bytes from Kafka into structured Flink data types.
    • Filtering: Discarding irrelevant events.
    • Mapping/Transforming: Changing data formats or enriching events with additional information.
    • Keying: Grouping events by a specific attribute (e.g., deviceId, userId) for stateful operations.
    • Windowing: Aggregating events that occur within a specific time frame (e.g., sum of temperatures every 5 minutes).
    • Stateful Operations: Maintaining and updating state for each key, such as counts, sums, or complex pattern detection.
    • Joins: Combining data from multiple streams or with static data.
  5. Kafka Cluster (Output Topics): After processing, the Flink application often writes its results to new Kafka topics (e.g., sensor_readings_processed, fraud_alerts). These output topics contain the transformed, aggregated, or enriched data, ready for downstream consumption. This pattern allows for building complex pipelines with multiple processing stages and enables different consumers to access different views of the data.
  6. Kafka Consumers (for Processed Data): Various downstream applications or services can subscribe to these output topics. This could include:
    • Dashboards/Monitoring Systems: Displaying real-time metrics.
    • Databases (e.g., Cassandra, PostgreSQL, Elasticsearch): Storing processed data for analytical queries or serving applications.
    • Alerting Systems: Triggering notifications (email, SMS, Slack) based on detected anomalies.
    • Machine Learning Models: Feeding real-time features for inference.
  7. External Sinks: The ultimate destination for your processed data. These could be operational databases, data warehouses, search engines, or custom applications.

This architecture is highly scalable because both Kafka and Flink are distributed systems designed for horizontal scaling. It's also fault-tolerant due to Kafka's replication and Flink's checkpointing mechanism, ensuring data integrity and continuous operation even when components fail. The use of Kafka as both input and output for Flink provides excellent decoupling and allows for flexible data flow management and replayability.

Hands-On: Building Your First Real-Time Pipeline

Enough theory! Let's get our hands dirty and build a simple, yet illustrative, real-time pipeline. We'll simulate IoT sensor data (temperature readings), ingest it into Kafka, process it with Flink to calculate average temperatures over time, and then consume the results.

For this example, we'll use Java for both Kafka producers/consumers and the Flink application. You'll need Java Development Kit (JDK) 8 or higher and Apache Maven installed.

Setting Up Your Environment (Local/Docker)

The easiest way to get Kafka and Flink running locally is using Docker Compose. Create a docker-compose.yml file:


version: '3.8'
services:
  zookeeper:
    image: confluentinc/cp-zookeeper:7.4.0
    hostname: zookeeper
    container_name: zookeeper
    ports:
      - "2181:2181"
    environment:
      ZOOKEEPER_CLIENT_PORT: 2181
      ZOOKEEPER_TICK_TIME: 2000

  kafka:
    image: confluentinc/cp-kafka:7.4.0
    hostname: kafka
    container_name: kafka
    ports:
      - "9092:9092"
    environment:
      KAFKA_BROKER_ID: 1
      KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
      KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:29092,PLAINTEXT_HOST://localhost:9092
      KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT
      KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT
      KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
      KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0
    depends_on:
      - zookeeper

  jobmanager:
    image: apache/flink:1.17.0-scala_2.12-java11
    hostname: jobmanager
    container_name: jobmanager
    ports:
      - "8081:8081"
    command: jobmanager
    environment:
      - &FLINK_PROPERTIES >
        jobmanager.rpc.address: jobmanager
        taskmanager.numberOfTaskSlots: 1
        blob.server.port: 6124
        query.server.port: 6125
        jobmanager.memory.process.size: 1600m
        taskmanager.memory.process.size: 1728m
      - FLINK_PROPERTIES_BLOB: *FLINK_PROPERTIES

  taskmanager:
    image: apache/flink:1.17.0-scala_2.12-java11
    hostname: taskmanager
    container_name: taskmanager
    command: taskmanager
    depends_on:
      - jobmanager
    environment:
      - &FLINK_PROPERTIES >
        jobmanager.rpc.address: jobmanager
        taskmanager.numberOfTaskSlots: 2 # Number of slots for parallelism
        blob.server.port: 6124
        query.server.port: 6125
        jobmanager.memory.process.size: 1600m
        taskmanager.memory.process.size: 1728m
      - FLINK_PROPERTIES_BLOB: *FLINK_PROPERTIES

Save this as docker-compose.yml and run docker-compose up -d. This will spin up a Zookeeper, a Kafka broker, a Flink JobManager, and a Flink TaskManager.

You can access the Flink UI at http://localhost:8081.

Maven Project Setup

Create a new Maven project. Here's a basic 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>
    <packaging>jar</packaging>

    <name>Flink Kafka Pipeline</name>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>11</maven.compiler.source>
        <maven.compiler.target>11</maven.compiler.target>
        <flink.version>1.17.0</flink.version>
        <kafka.version>3.5.0</kafka.version>
    </properties>

    <dependencies>
        <!-- Apache Flink -->
        <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-java</artifactId>
            <version>${flink.version}</version>
            <scope>provided</scope> <!-- Important: Flink dependencies are provided by the cluster -->
        </dependency>
        <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-streaming-java</artifactId>
            <version>${flink.version}</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-connector-kafka</artifactId>
            <version>${flink.version}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-json</artifactId>
            <version>${flink.version}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-clients</artifactId>
            <version>${flink.version}</version>
            <scope>provided</scope>
        </dependency>

        <!-- Apache Kafka Client -->
        <dependency>
            <groupId>org.apache.kafka</groupId>
            <artifactId>kafka-clients</artifactId>
            <version>${kafka.version}</version>
        </dependency>

        <!-- Logging -->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-log4j12</artifactId>
            <version>1.7.36</version>
        </dependency>

        <!-- JSON Processing -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.15.2</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <!-- Java Compiler Plugin -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <source>${maven.compiler.source}</source>
                    <target>${maven.compiler.target}</target>
                </configuration>
            </plugin>
            <!-- Shade Plugin for Flink -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-shade-plugin</artifactId>
                <version>3.2.4</version>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>shade</goal>
                        </goals>
                        <configuration>
                            <artifactSet>
                                <excludes>
                                    <!-- Exclude Flink and Scala dependencies as they are provided by the cluster -->
                                    <exclude>org.apache.flink:*</exclude>
                                    <exclude>org.scala-lang:*</exclude>
                                </excludes>
                            </artifactSet>
                            <filters>
                                <filter>
                                    <artifact>*:*</artifact>
                                    <excludes>
                                        <!-- Exclude signing files to prevent issues -->
                                        <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.FlinkTemperatureProcessor</mainClass> <!-- Set your Flink main class -->
                                </transformer>
                            </transformers>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

Important Note on Flink Dependencies: Notice the <scope>provided</scope> for Flink core dependencies. This tells Maven that these dependencies will be provided by the Flink cluster itself and should not be bundled into our application JAR. The Kafka connector and JSON dependencies, however, need to be bundled, which is handled by the maven-shade-plugin.

Step 1: Data Generation and Ingestion (Kafka Producer)

We'll create a simple Java application that simulates IoT devices sending temperature readings to a Kafka topic named sensor_temperatures_raw. Each message will be a JSON string.


package com.khadervali.streaming;

import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.common.serialization.StringSerializer;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;

import java.util.Properties;
import java.util.Random;
import java.time.Instant;

public class TemperatureSensorProducer {

private static final String TOPIC_NAME = "sensor_temperatures_raw";
private static final String BOOTSTRAP_SERVERS = "localhost:9092"; // Kafka broker address

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

Written by

Khader Vali

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

Share this article

Related Articles

Saga Pattern for Distributed Transactions in Microservices

Jun 30, 2026 · 15 min read

Scalable WebSockets: Building Real-Time Infrastructure

Jul 07, 2026 · 18 min read

Full Stack TypeScript: tRPC & Prisma End-to-End

May 26, 2026 · 10 min read