Episode ini membahas Kafka Streams: library stream processing dengan KStream, KTable, dan GlobalKTable, topologi stream, operasi DSL seperti filter dan aggregate, state store berbasis RocksDB, windowing, serta processing guarantee exactly-once.

Selama ini kalian membaca dan menulis data dengan producer dan consumer. Bagaimana jika kalian ingin menghitung, menggabungkan, atau mengagregasi data saat mengalir? Kafka Streams adalah library Java yang mengubah Kafka menjadi mesin stream processing — tanpa membutuhkan cluster terpisah seperti Flink atau Spark Streaming.
Keunggulan Kafka Streams: ia berjalan sebagai aplikasi biasa di JVM, memakai Kafka sebagai storage state, dan mewarisi semua jaminan Kafka termasuk exactly-once. Tidak ada infrastruktur baru; kalian menulis topologi, dan library menangani parallelism, failover, dan recovery.
Episode 13 ini akan membahas konsep dasar KStream, KTable, dan GlobalKTable, operasi DSL, stateful processing dengan RocksDB, windowing untuk agregasi waktu, serta processing guarantee exactly-once.
Perbedaan penting: Kafka Streams adalah library, bukan framework server. Kalian tidak menjalankan daemon khusus; aplikasi kalian adalah sebuah JVM process yang memakai Kafka Streams. Ini berarti deployment standar: jar, image Docker, atau orchestrator mana pun. Satu aplikasi bisa di-scale dengan menjalankan banyak instance; partisi topic dibagi di antara instance.
Tiga abstraksi utama untuk merepresentasikan data:
Setiap aplikasi Streams mendefinisikan topology: graf prosesor yang menghubungkan source (topic input), operator (filter, map, aggregate), dan sink (topic output). Topologi ini dibangun dengan Streams DSL, lalu dijalankan oleh KafkaStreams:
StreamsBuilder builder = new StreamsBuilder();
KStream<String, String> orders = builder.stream("orders");
orders.filter((key, value) -> value.contains("PAID"))
.mapValues(value -> value.toUpperCase())
.to("paid-orders");
KafkaStreams streams = new KafkaStreams(builder.build(), props);
streams.start();builder.stream("orders") membuat source dari topic, dan setiap operator mengalirkan data ke operator berikutnya sampai to() menulis hasil ke topic tujuan.
Operasi tanpa state memproses satu record tanpa mengingat data lain:
Operasi ber-state membutuhkan data lama: groupBy dan aggregate mengelompokkan record dan menghitung agregasi; join menggabungkan dua aliran berdasarkan key. Contoh agregasi:
KTable<String, Long> orderCount = orders
.filter((key, value) -> value.contains("PAID"))
.groupByKey()
.count(Materialized.as("order-count-store"));groupByKey().count() menghitung jumlah record per key dan menyimpan hasilnya di state store bernama order-count-store — sebuah KTable yang nilai terbarunya bisa di-query.
Hasil agregasi dan join disimpan di state store. Default penyimpanan lokal adalah RocksDB, database embedded yang cepat untuk key-value. Setiap instance aplikasi memiliki salinan state store untuk partition yang menjadi tanggung jawabnya.
Setiap state store memiliki changelog topic (ber-cleanup.policy=compact, lihat episode 10) yang mencatat setiap perubahan state. Ketika instance crash atau rebalance, instance baru memulihkan state dengan membaca changelog dari posisi terakhir — inilah yang membuat stream processing toleran terhadap kegagalan tanpa kehilangan state.
State yang tersimpan tidak hanya untuk internal — bisa di-query langsung oleh aplikasi lain melalui Interactive Queries:
curl -s "http://localhost:8080/state/order-count-store/key/order-001"curl -s http://localhost:8080/state/order-count-store membaca nilai terkini dari state store. Karena state terdistribusi, aplikasi perlu menanyakan lokasi partition via API metadata sebelum mengambil nilai — pola yang memungkinkan Kafka Streams berfungsi sebagai materialized view real-time.
Agregasi waktu membagi aliran menjadi window:
Streaming data sering datang terlambat. Grace period menentukan berapa lama record yang terlambat masih diterima setelah window berakhir. Setelah grace period lewat, record dianggap terlambat dan dibuang:
KTable<Windowed<String>, Long> perMinute = orders
.groupByKey()
.windowedBy(TimeWindows.of(Duration.ofMinutes(1)).grace(Duration.ofSeconds(10)))
.count();TimeWindows.of(Duration.ofMinutes(1)) membuat tumbling window satu menit, dan .grace(Duration.ofSeconds(10)) memberi toleransi keterlambatan sepuluh detik.
Kafka Streams mengintegrasikan transactions (episode 9) secara native. Konfigurasi:
processing.guarantee=exactly_once_v2Dengan processing.guarantee=exactly_once_v2, Streams memakai producer transaksional dan consumer read_committed secara otomatis: offset, state, dan output di-commit dalam transaksi yang sama, sehingga proses ulang tidak menghasilkan duplikat.
Secara konkret: ketika instance memproses batch, semua tulis (ke topic output dan changelog) plus offset konsumsi berada dalam satu transaksi. Jika crash terjadi, batch diproses ulang dari awal tetapi transaksi lama di-abort — hasilnya sama, tidak ada duplikat. Ini adalah exactly-once end-to-end yang paling mudah dicapai di ekosistem Kafka, tanpa perlu menulis kode transaksi manual.
Warning
Skala aplikasi dengan menjalankan instance tambahan memang mudah, tetapi application.id (identitas aplikasi) harus sama di semua instance agar mereka dianggap satu consumer group. Mengganti application.id akan memulai ulang state store dari nol.
Di episode 13 ini kalian sudah memahami Kafka Streams sebagai library stream processing, perbedaan KStream, KTable, dan GlobalKTable, operasi stateless dan stateful, state store dengan RocksDB dan changelog topic, windowing dengan grace period, serta exactly-once processing.
Inti yang harus dibawa pulang:
processing.guarantee=exactly_once_v2 memberi EOS end-to-end otomatis.application.id yang sama.Di episode 14 selanjutnya kita akan membahas ksqlDB — SQL untuk stream processing yang duduk di atas Kafka Streams. Kalian akan belajar perbedaan stream dan table, persistent query, push dan pull query, serta deployment server dan CLI-nya.