Build production-ready Kafka Streams applications using Java and Scala for real-time data processing, including stateless transformations, stateful aggregations, windowing, and stream joins with exactly-once semantics.
Prerequisites
- Java 17 or later installed
- Apache Kafka cluster running
- Basic knowledge of stream processing concepts
- Understanding of JSON data formats
What this solves
Kafka Streams enables real-time data processing directly within your applications without requiring separate stream processing clusters. This tutorial shows you how to build production-grade stream processing applications using both Java and Scala, covering stateless operations, stateful processing with windowing, stream joins, and exactly-once semantics for mission-critical data pipelines.
Step-by-step installation
Install Java development environment
Kafka Streams requires Java 8 or later. Install OpenJDK and verify the installation.
sudo apt update
sudo apt install -y openjdk-17-jdk maven gradle
java -version
mvn -version
sudo dnf update -y
sudo dnf install -y java-17-openjdk-devel maven gradle
java -version
mvn -version
Install Scala development environment
Install Scala and SBT for Scala-based Kafka Streams development.
curl -fL https://github.com/coursier/launchers/raw/master/cs-x86_64-pc-linux.gz | gzip -d > cs
chmod +x cs
sudo mv cs /usr/local/bin
cs setup --yes
echo 'export PATH="$PATH:$HOME/.local/share/coursier/bin"' >> ~/.bashrc
source ~/.bashrc
scala -version
sbt --version
curl -fL https://github.com/coursier/launchers/raw/master/cs-x86_64-pc-linux.gz | gzip -d > cs
chmod +x cs
sudo mv cs /usr/local/bin
cs setup --yes
echo 'export PATH="$PATH:$HOME/.local/share/coursier/bin"' >> ~/.bashrc
source ~/.bashrc
scala -version
sbt --version
Set up Kafka cluster
Install and configure Apache Kafka for stream processing. We'll use the binary distribution for simplicity.
cd /opt
sudo wget https://downloads.apache.org/kafka/2.8.2/kafka_2.13-2.8.2.tgz
sudo tar -xzf kafka_2.13-2.8.2.tgz
sudo mv kafka_2.13-2.8.2 kafka
sudo chown -R $USER:$USER /opt/kafka
echo 'export KAFKA_HOME=/opt/kafka' >> ~/.bashrc
echo 'export PATH=$PATH:$KAFKA_HOME/bin' >> ~/.bashrc
source ~/.bashrc
Configure and start Kafka services
Start Zookeeper and Kafka broker with production-ready configurations.
cd /opt/kafka
# Start Zookeeper
bin/zookeeper-server-start.sh -daemon config/zookeeper.properties
# Wait for Zookeeper to start
sleep 5
# Start Kafka broker
bin/kafka-server-start.sh -daemon config/server.properties
# Verify services are running
jps | grep -E '(QuorumPeerMain|Kafka)'
Create test topics
Create input and output topics for stream processing examples with multiple partitions for parallelism.
cd /opt/kafka
# Create input topic for user events
bin/kafka-topics.sh --create --topic user-events \
--bootstrap-server localhost:9092 \
--partitions 3 \
--replication-factor 1
# Create output topic for processed events
bin/kafka-topics.sh --create --topic processed-events \
--bootstrap-server localhost:9092 \
--partitions 3 \
--replication-factor 1
# Create topic for aggregated results
bin/kafka-topics.sh --create --topic user-aggregates \
--bootstrap-server localhost:9092 \
--partitions 3 \
--replication-factor 1
# List created topics
bin/kafka-topics.sh --list --bootstrap-server localhost:9092
Build stateless stream processing applications
Create Java Maven project structure
Set up a Maven project for Java-based Kafka Streams applications with proper dependencies.
mkdir -p ~/kafka-streams-java/src/main/java/com/example/streams
cd ~/kafka-streams-java
Implement Java stateless stream processor
Create a stream processor that filters and transforms user events with JSON serialization.
package com.example.streams;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
public class UserEvent {
@JsonProperty("userId")
public String userId;
@JsonProperty("eventType")
public String eventType;
@JsonProperty("timestamp")
public long timestamp;
@JsonProperty("value")
public double value;
@JsonProperty("metadata")
public String metadata;
private static final ObjectMapper objectMapper = new ObjectMapper();
public UserEvent() {}
public UserEvent(String userId, String eventType, long timestamp, double value, String metadata) {
this.userId = userId;
this.eventType = eventType;
this.timestamp = timestamp;
this.value = value;
this.metadata = metadata;
}
public String toJson() throws JsonProcessingException {
return objectMapper.writeValueAsString(this);
}
public static UserEvent fromJson(String json) throws JsonProcessingException {
return objectMapper.readValue(json, UserEvent.class);
}
public boolean isValidEvent() {
return userId != null && eventType != null && value >= 0;
}
public UserEvent enrich() {
return new UserEvent(
this.userId,
this.eventType.toUpperCase(),
this.timestamp,
this.value,
"processed_" + this.metadata
);
}
}
Create Java stream processing topology
Implement the main stream processing application with filtering, mapping, and branching logic.
package com.example.streams;
import org.apache.kafka.common.serialization.Serdes;
import org.apache.kafka.streams.KafkaStreams;
import org.apache.kafka.streams.StreamsBuilder;
import org.apache.kafka.streams.StreamsConfig;
import org.apache.kafka.streams.kstream.KStream;
import org.apache.kafka.streams.kstream.Named;
import org.apache.kafka.streams.kstream.Predicate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Properties;
import java.util.concurrent.CountDownLatch;
public class UserEventProcessor {
private static final Logger logger = LoggerFactory.getLogger(UserEventProcessor.class);
public static void main(String[] args) {
Properties props = new Properties();
props.put(StreamsConfig.APPLICATION_ID_CONFIG, "user-event-processor");
props.put(StreamsConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
props.put(StreamsConfig.DEFAULT_KEY_SERDE_CLASS_CONFIG, Serdes.String().getClass());
props.put(StreamsConfig.DEFAULT_VALUE_SERDE_CLASS_CONFIG, Serdes.String().getClass());
props.put(StreamsConfig.PROCESSING_GUARANTEE_CONFIG, StreamsConfig.EXACTLY_ONCE_V2);
props.put(StreamsConfig.NUM_STREAM_THREADS_CONFIG, 2);
StreamsBuilder builder = new StreamsBuilder();
// Read from input topic
KStream
Implement stateful stream processing
Create Scala SBT project
Set up a Scala project for advanced stream processing with aggregations and windowing.
mkdir -p ~/kafka-streams-scala/src/main/scala/com/example/streams
cd ~/kafka-streams-scala
ThisBuild / version := "1.0.0"
ThisBuild / scalaVersion := "2.13.11"
val kafkaVersion = "3.6.0"
val jacksonVersion = "2.15.2"
lazy val root = (project in file("."))
.settings(
name := "kafka-streams-scala",
libraryDependencies ++= Seq(
"org.apache.kafka" %% "kafka-streams-scala" % kafkaVersion,
"org.apache.kafka" % "kafka-streams" % kafkaVersion,
"com.fasterxml.jackson.core" % "jackson-databind" % jacksonVersion,
"com.fasterxml.jackson.module" %% "jackson-module-scala" % jacksonVersion,
"ch.qos.logback" % "logback-classic" % "1.4.11",
"org.scalatest" %% "scalatest" % "3.2.17" % Test,
"org.apache.kafka" % "kafka-streams-test-utils" % kafkaVersion % Test
),
assembly / mainClass := Some("com.example.streams.UserAggregateProcessor"),
assembly / assemblyJarName := "kafka-streams-scala-assembly.jar",
assembly / assemblyMergeStrategy := {
case "META-INF/services/org.apache.kafka.common.config.ConfigDef" => MergeStrategy.concat
case PathList("META-INF", xs @ _*) => MergeStrategy.discard
case _ => MergeStrategy.first
}
)
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "2.1.3")
Implement Scala stateful processor with windowing
Create an advanced stream processor that performs windowed aggregations and maintains state.
package com.example.streams
import org.apache.kafka.streams.scala._
import org.apache.kafka.streams.scala.kstream._
import org.apache.kafka.streams.{KafkaStreams, StreamsConfig}
import org.apache.kafka.streams.kstream.{TimeWindows, Windowed}
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.scala.DefaultScalaModule
import org.slf4j.LoggerFactory
import java.time.Duration
import java.util.Properties
import scala.concurrent.duration._
import scala.util.{Failure, Success, Try}
case class UserEvent(
userId: String,
eventType: String,
timestamp: Long,
value: Double,
metadata: String
)
case class UserAggregate(
userId: String,
eventCount: Long,
totalValue: Double,
avgValue: Double,
maxValue: Double,
minValue: Double,
lastEventTime: Long
)
object UserAggregateProcessor {
private val logger = LoggerFactory.getLogger(this.getClass)
private val objectMapper = new ObjectMapper()
objectMapper.registerModule(DefaultScalaModule)
implicit val consumed: Consumed[String, String] = Consumed.`with`(Serdes.stringSerde, Serdes.stringSerde)
implicit val produced: Produced[String, String] = Produced.`with`(Serdes.stringSerde, Serdes.stringSerde)
def parseUserEvent(json: String): Option[UserEvent] = {
Try {
objectMapper.readValue(json, classOf[UserEvent])
} match {
case Success(event) if event.userId != null && event.eventType != null => Some(event)
case Success(_) =>
logger.warn(s"Invalid event format: $json")
None
case Failure(e) =>
logger.warn(s"Failed to parse event: $json", e)
None
}
}
def serializeAggregate(aggregate: UserAggregate): String = {
objectMapper.writeValueAsString(aggregate)
}
def main(args: Array[String]): Unit = {
val props = new Properties()
props.put(StreamsConfig.APPLICATION_ID_CONFIG, "user-aggregate-processor")
props.put(StreamsConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092")
props.put(StreamsConfig.PROCESSING_GUARANTEE_CONFIG, StreamsConfig.EXACTLY_ONCE_V2)
props.put(StreamsConfig.NUM_STREAM_THREADS_CONFIG, "3")
props.put(StreamsConfig.COMMIT_INTERVAL_MS_CONFIG, "1000")
props.put(StreamsConfig.CACHE_MAX_BYTES_BUFFERING_CONFIG, "10240")
val builder = new StreamsBuilder()
val userEvents: KStream[String, String] = builder.stream<a>String, String</a>
// Parse and key by userId
val validEvents: KStream[String, UserEvent] = userEvents
.flatMapValues(parseUserEvent)
.selectKey((_, event) => event.userId)
// Create 5-minute tumbling windows for aggregation
val windowedAggregates: KTable[Windowed[String], UserAggregate] = validEvents
.groupByKey
.windowedBy(TimeWindows.of(Duration.ofMinutes(5)).advanceBy(Duration.ofMinutes(1)))
.aggregate(
// Initializer
() => UserAggregate("", 0L, 0.0, 0.0, Double.MinValue, Double.MaxValue, 0L)
)(
// Aggregator
(key: String, event: UserEvent, aggregate: UserAggregate) => {
val newCount = aggregate.eventCount + 1
val newTotal = aggregate.totalValue + event.value
val newAvg = newTotal / newCount
val newMax = math.max(aggregate.maxValue, event.value)
val newMin = if (aggregate.minValue == Double.MaxValue) event.value else math.min(aggregate.minValue, event.value)
UserAggregate(
userId = event.userId,
eventCount = newCount,
totalValue = newTotal,
avgValue = newAvg,
maxValue = newMax,
minValue = newMin,
lastEventTime = math.max(aggregate.lastEventTime, event.timestamp)
)
}
)
// Output windowed aggregates
windowedAggregates
.toStream
.filter((_, aggregate) => aggregate.eventCount > 0)
.mapValues(serializeAggregate)
.peek((windowedKey, aggregate) => {
val window = windowedKey.window()
logger.info(s"Aggregate for user ${windowedKey.key()} in window [${window.start()}-${window.end()}]: $aggregate")
})
.selectKey((windowedKey, _) => windowedKey.key())
.to("user-aggregates")
// Session windows for detecting user activity sessions
val sessionAggregates = validEvents
.filter((_, event) => event.eveAutomated install script
Run this to automate the entire setup
#!/usr/bin/env bash
set -euo pipefail
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Global variables
KAFKA_VERSION="2.8.2"
SCALA_VERSION="2.13"
KAFKA_HOME="/opt/kafka"
INSTALL_USER="${SUDO_USER:-$(whoami)}"
INSTALL_HOME=$(getent passwd "$INSTALL_USER" | cut -d: -f6)
# Usage message
usage() {
echo "Usage: $0 [OPTIONS]"
echo "Options:"
echo " -h, --help Show this help message"
echo " -v, --version Kafka version to install (default: $KAFKA_VERSION)"
exit 1
}
# Parse arguments
while [[ $# -gt 0 ]]; do
case $1 in
-h|--help)
usage
;;
-v|--version)
KAFKA_VERSION="$2"
shift 2
;;
*)
echo -e "${RED}Error: Unknown option $1${NC}"
usage
;;
esac
done
# Cleanup function
cleanup() {
echo -e "${RED}Installation failed. Cleaning up...${NC}"
# Stop services if they were started
pkill -f "kafka.Kafka" || true
pkill -f "QuorumPeerMain" || true
# Remove partial installations
rm -rf /opt/kafka* || true
exit 1
}
trap cleanup ERR
# Check prerequisites
check_prerequisites() {
echo -e "${BLUE}[1/8] Checking prerequisites...${NC}"
if [[ $EUID -ne 0 ]]; then
echo -e "${RED}Error: This script must be run as root or with sudo${NC}"
exit 1
fi
# Detect distribution
if [ -f /etc/os-release ]; then
. /etc/os-release
case "$ID" in
ubuntu|debian)
PKG_MGR="apt"
PKG_UPDATE="apt update"
PKG_INSTALL="apt install -y"
JAVA_PKG="openjdk-17-jdk"
;;
almalinux|rocky|centos|rhel|ol|fedora)
PKG_MGR="dnf"
PKG_UPDATE="dnf update -y"
PKG_INSTALL="dnf install -y"
JAVA_PKG="java-17-openjdk-devel"
;;
amzn)
PKG_MGR="yum"
PKG_UPDATE="yum update -y"
PKG_INSTALL="yum install -y"
JAVA_PKG="java-17-openjdk-devel"
;;
*)
echo -e "${RED}Error: Unsupported distribution: $ID${NC}"
exit 1
;;
esac
else
echo -e "${RED}Error: Cannot detect Linux distribution${NC}"
exit 1
fi
echo -e "${GREEN}Prerequisites checked. Distribution: $ID${NC}"
}
# Install Java development environment
install_java() {
echo -e "${BLUE}[2/8] Installing Java development environment...${NC}"
$PKG_UPDATE
$PKG_INSTALL $JAVA_PKG maven gradle wget curl gzip tar
# Verify Java installation
java -version
mvn -version
echo -e "${GREEN}Java development environment installed successfully${NC}"
}
# Install Scala development environment
install_scala() {
echo -e "${BLUE}[3/8] Installing Scala development environment...${NC}"
# Install Coursier launcher
curl -fL https://github.com/coursier/launchers/raw/master/cs-x86_64-pc-linux.gz | gzip -d > cs
chmod 755 cs
mv cs /usr/local/bin/
# Setup Scala as the install user
sudo -u "$INSTALL_USER" bash -c "
export HOME='$INSTALL_HOME'
/usr/local/bin/cs setup --yes
echo 'export PATH=\"\$PATH:\$HOME/.local/share/coursier/bin\"' >> '$INSTALL_HOME/.bashrc'
"
# Verify Scala installation
sudo -u "$INSTALL_USER" bash -c "
export HOME='$INSTALL_HOME'
export PATH='\$PATH:$INSTALL_HOME/.local/share/coursier/bin'
scala -version || echo 'Scala will be available after sourcing .bashrc'
"
echo -e "${GREEN}Scala development environment installed successfully${NC}"
}
# Install Kafka cluster
install_kafka() {
echo -e "${BLUE}[4/8] Installing Kafka cluster...${NC}"
cd /opt
wget "https://downloads.apache.org/kafka/${KAFKA_VERSION}/kafka_${SCALA_VERSION}-${KAFKA_VERSION}.tgz"
tar -xzf "kafka_${SCALA_VERSION}-${KAFKA_VERSION}.tgz"
mv "kafka_${SCALA_VERSION}-${KAFKA_VERSION}" kafka
rm "kafka_${SCALA_VERSION}-${KAFKA_VERSION}.tgz"
# Set proper ownership
chown -R "$INSTALL_USER:$INSTALL_USER" "$KAFKA_HOME"
chmod -R 755 "$KAFKA_HOME"
# Add Kafka to PATH for the install user
sudo -u "$INSTALL_USER" bash -c "
echo 'export KAFKA_HOME=$KAFKA_HOME' >> '$INSTALL_HOME/.bashrc'
echo 'export PATH=\$PATH:\$KAFKA_HOME/bin' >> '$INSTALL_HOME/.bashrc'
"
echo -e "${GREEN}Kafka installed successfully${NC}"
}
# Configure and start Kafka services
start_kafka_services() {
echo -e "${BLUE}[5/8] Starting Kafka services...${NC}"
cd "$KAFKA_HOME"
# Start Zookeeper as the install user
sudo -u "$INSTALL_USER" bash -c "
cd '$KAFKA_HOME'
bin/zookeeper-server-start.sh -daemon config/zookeeper.properties
"
# Wait for Zookeeper to start
sleep 10
# Start Kafka broker as the install user
sudo -u "$INSTALL_USER" bash -c "
cd '$KAFKA_HOME'
bin/kafka-server-start.sh -daemon config/server.properties
"
# Wait for Kafka to start
sleep 15
# Verify services are running
if pgrep -f "QuorumPeerMain" > /dev/null && pgrep -f "kafka.Kafka" > /dev/null; then
echo -e "${GREEN}Kafka services started successfully${NC}"
else
echo -e "${RED}Error: Kafka services failed to start${NC}"
exit 1
fi
}
# Create test topics
create_test_topics() {
echo -e "${BLUE}[6/8] Creating test topics...${NC}"
sudo -u "$INSTALL_USER" bash -c "
cd '$KAFKA_HOME'
# Create input topic for user events
bin/kafka-topics.sh --create --topic user-events \
--bootstrap-server localhost:9092 \
--partitions 3 \
--replication-factor 1
# Create output topic for processed events
bin/kafka-topics.sh --create --topic processed-events \
--bootstrap-server localhost:9092 \
--partitions 3 \
--replication-factor 1
# Create topic for aggregated results
bin/kafka-topics.sh --create --topic user-aggregates \
--bootstrap-server localhost:9092 \
--partitions 3 \
--replication-factor 1
"
echo -e "${GREEN}Test topics created successfully${NC}"
}
# Create Java project structure
create_java_project() {
echo -e "${BLUE}[7/8] Creating Java project structure...${NC}"
sudo -u "$INSTALL_USER" bash -c "
mkdir -p '$INSTALL_HOME/kafka-streams-java/src/main/java/com/example/streams'
cd '$INSTALL_HOME/kafka-streams-java'
cat > pom.xml << 'EOF'
<?xml version=\"1.0\" encoding=\"UTF-8\"?>
<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.example</groupId>
<artifactId>kafka-streams-java</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<kafka.version>3.6.0</kafka.version>
<jackson.version>2.15.2</jackson.version>
</properties>
<dependencies>
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-streams</artifactId>
<version>\${kafka.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>\${jackson.version}</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>2.0.9</version>
</dependency>
</dependencies>
</project>
EOF
"
echo -e "${GREEN}Java project structure created successfully${NC}"
}
# Final verification
verify_installation() {
echo -e "${BLUE}[8/8] Verifying installation...${NC}"
# Check Java
if ! java -version &>/dev/null; then
echo -e "${RED}Error: Java verification failed${NC}"
exit 1
fi
# Check Kafka services
if ! pgrep -f "QuorumPeerMain" > /dev/null || ! pgrep -f "kafka.Kafka" > /dev/null; then
echo -e "${RED}Error: Kafka services are not running${NC}"
exit 1
fi
# List topics to verify Kafka is working
sudo -u "$INSTALL_USER" bash -c "
cd '$KAFKA_HOME'
bin/kafka-topics.sh --list --bootstrap-server localhost:9092
" > /dev/null
echo -e "${GREEN}Installation completed successfully!${NC}"
echo -e "${YELLOW}Next steps:${NC}"
echo "1. Source the bashrc file: source $INSTALL_HOME/.bashrc"
echo "2. Navigate to the Java project: cd $INSTALL_HOME/kafka-streams-java"
echo "3. Build the project: mvn compile"
echo "4. Kafka is running on localhost:9092"
echo "5. Topics created: user-events, processed-events, user-aggregates"
}
# Main execution
main() {
echo -e "${GREEN}Starting Kafka Streams development environment installation...${NC}"
check_prerequisites
install_java
install_scala
install_kafka
start_kafka_services
create_test_topics
create_java_project
verify_installation
echo -e "${GREEN}Kafka Streams development environment is ready!${NC}"
}
main "$@"
Review the script before running. Execute with: bash install.sh