Pipeline yang kita bangun sepanjang course ini hampir selalu bekerja atas berkas dan tabel yang sudah selesai ditulis: ekspor pesanan TokoKita per hari, snapshot stok per jam, hasil query yang berhenti pada satu titik. Aplikasi TokoKita sendiri tidak menghasilkan data secara periodik — aplikasi tersebut melepas event pesanan satu per satu, dua puluh empat jam sehari, tanpa ada yang tahu kapan aliran tersebut berhenti. Stream processing adalah cabang pemrosesan data yang dirancang untuk aliran seperti ini: data diproses begitu tiba, hasil dihitung secara berkelanjutan, dan sistem tidak boleh berasumsi data sudah lengkap. Chapter ini meletakkan tiga fondasi yang dipakai sepanjang section: unbounded data, anatomi sistem stream, dan state yang diperbarui secara increment.

Data Bounded dan Unbounded #

Bounded data adalah kumpulan record yang berawal dan berakhir: berkas ekspor pesanan 15 Januari selesai diunggah dan isinya tidak berubah lagi. Ukurannya diketahui di muka, dan job batch pada section Pemrosesan Data Skala Besar mengolahnya dari baris pertama sampai baris terakhir dalam satu eksekusi. Unbounded data bekerja sebaliknya: jumlah barisnya tidak diketahui, tidak ada baris terakhir, dan setiap saat event baru bisa menyusul dari belakang.

AspekBounded dataUnbounded data
Titik akhirada — berkas atau tabel selesai ditulistidak ada — event terus menyusul
Ukuran datadiketahui sebelum diprosestidak diketahui, tumbuh sepanjang waktu
Pola pengolahanbaca semua, proses sekali jalanproses per event begitu event tiba
Bentuk hasilfinal setelah job selesaisnapshot yang terus diperbarui
Contoh TokoKitaekspor pesanan harian untuk warehouseevent pesanan langsung dari aplikasi

Konsekuensi terpenting ada pada baris terakhir tabel: hasil olahan stream tidak pernah final. Angka “total penjualan hari ini” yang dihitung dari stream adalah snapshot — nilai total penjualan sampai detik angka tersebut dihitung, dan angka tersebut akan kedaluwarsa begitu pesanan berikutnya tiba.

Unbounded juga tidak berarti kita harus mengingat seluruh histori. Pertanyaan analitis hampir selalu dibatasi jendela waktu — total penjualan per menit, rata-rata per jam — sehingga state yang perlu dipelihara tetap kecil meski aliran datanya tidak pernah berakhir. Mekanisme jendela tersebut kita bahas tuntas pada Chapter Windowing.

Anatomi Sistem Stream Processing #

Sistem stream processing tersusun dari empat komponen. Source adalah aliran masukan — topic Kafka atau stream perubahan tabel (CDC). Stream processor membaca event satu per satu dan menjalankan logika: memfilter, mengagregasi, atau menggabungkan dengan stream lain. State menyimpan hasil antara yang terus diperbarui, misalnya total penjualan berjalan per kota. Sink adalah tujuan keluaran: dashboard, tabel hasil, sistem peringatan, atau topic lain yang dibaca sistem berikutnya.

State adalah komponen yang paling membedakan stream dari batch. Job batch menyimpan hasil antara di memori selama satu eksekusi lalu membuangnya saat job selesai; stream processor harus menjaga state tetap hidup berminggu-minggu dan mampu memulihkan state setelah kegagalan. Perbedaan waktu kejadian dan waktu pemrosesan juga baru terasa di stream — event pesanan pukul 09:00 bisa diproses pukul 09:02 karena antrean; dampaknya kita bedah tuntas pada Chapter Event Time vs Processing Time.

Simulasi Konsumsi Stream Tak Berujung #

Generator Python adalah model mental yang tepat untuk stream: objek tersebut bersifat lazy, menghasilkan event satu per satu saat diminta, dan tidak pernah berhenti sendiri — persis seperti aliran pesanan TokoKita. Simulasi berikut dikonsumsi secara bertahap sambil memelihara statistik berjalan.

python
"""Stream tak berujung: generator pesanan + konsumen dengan statistik berjalan."""

import itertools
import random

PELANGGAN = ["budi", "siti", "dewi", "anton", "rina"]


def stream_pesanan():
    """Generator event pesanan TokoKita — tidak pernah berhenti sendiri."""
    acak = random.Random(42)
    for nomor in itertools.count(start=1):
        yield {
            "order_id": f"ORD-{nomor:05d}",
            "pelanggan": acak.choice(PELANGGAN),
            "nilai": acak.randrange(50, 950) * 1000,  # Rp50.000 - Rp949.000
        }


def main():
    stat = {"jumlah": 0, "total": 0, "terbesar": 0}
    sumber = stream_pesanan()  # satu iterator, dikonsumsi bertahap

    print("Konsumsi 8 event pertama dari stream tak berujung:")
    for event in itertools.islice(sumber, 8):
        stat["jumlah"] += 1
        stat["total"] += event["nilai"]
        stat["terbesar"] = max(stat["terbesar"], event["nilai"])
        rata_rata = stat["total"] / stat["jumlah"]
        print(f'  {event["order_id"]} {event["pelanggan"]:<5} '
              f'Rp{event["nilai"]:>8,} -> jumlah={stat["jumlah"]:>2}, '
              f"rata-rata Rp{rata_rata:,.0f}")

    print(f"\nSnapshot setelah 8 event : total Rp{stat['total']:,}, "
          f"terbesar Rp{stat['terbesar']:,}")
    print("Stream belum selesai — data berikutnya sudah tersedia tanpa menunggu.")

    # Buktikan stream masih hidup: lanjutkan dari iterator yang sama
    print("Event berikutnya tetap mengalir:", next(sumber)["order_id"])


if __name__ == "__main__":
    main()

Output yang Dihasilkan:

text
Konsumsi 8 event pertama dari stream tak berujung:
  ORD-00001 budi  Rp  75,000 -> jumlah= 1, rata-rata Rp75,000
  ORD-00002 dewi  Rp 300,000 -> jumlah= 2, rata-rata Rp187,500
  ORD-00003 siti  Rp 192,000 -> jumlah= 3, rata-rata Rp189,000
  ORD-00004 budi  Rp 742,000 -> jumlah= 4, rata-rata Rp327,250
  ORD-00005 rina  Rp 139,000 -> jumlah= 5, rata-rata Rp289,600
  ORD-00006 rina  Rp 482,000 -> jumlah= 6, rata-rata Rp321,667
  ORD-00007 budi  Rp  80,000 -> jumlah= 7, rata-rata Rp287,143
  ORD-00008 budi  Rp 273,000 -> jumlah= 8, rata-rata Rp285,375

Snapshot setelah 8 event : total Rp2,283,000, terbesar Rp742,000
Stream belum selesai — data berikutnya sudah tersedia tanpa menunggu.
Event berikutnya tetap mengalir: ORD-00009

Mari kita telaah baris-baris kode di atas:

  1. Baris 9-17 (stream_pesanan): itertools.count(start=1) tidak pernah habis, sehingga generator tidak pernah menaikkan StopIteration dengan sendirinya — inilah unbounded data dalam bentuk paling murni.
  2. Baris 25 (itertools.islice(sumber, 8)): konsumen berhenti membaca setelah 8 event karena kebutuhan analisis sudah terpenuhi, bukan karena stream selesai.
  3. Baris 26-28 (pembaruan stat): jumlah, total, dan nilai terbesar diperbarui per event — tidak ada perhitungan ulang dari awal, karena event yang sudah lewat tidak disimpan.
  4. Baris 29 (rata_rata = ...): hasil antara dihitung dari state berjalan, bukan dari agregasi seluruh data; inilah snapshot yang selalu siap dibaca.
  5. Baris 39 (next(sumber)): event kesembilan tersedia seketika tanpa menunggu jendela waktu — sinyal bahwa sumber masih hidup dan terus mengalir.

Tantangan Utama Stream Processing #

Tiga masalah berikut hampir tidak pernah muncul di pipeline batch, tetapi menjadi makanan sehari-hari sistem stream:

  • Urutan dan keterlambatan event — event pukul 09:00 bisa tiba setelah event pukul 09:02 karena jalur jaringan berbeda; processor harus punya aturan untuk event telat.
  • Laju sumber melebihi laju pengolahan — saat ramai flash sale, event menumpuk lebih cepat daripada kecepatan pemrosesan; antrean di tengah menjadi penyangga dan sumber backpressure.
  • Duplikasi dan kegagalan — pengiriman ulang setelah gangguan jaringan membuat event yang sama bisa diproses dua kali; setiap hasil harus tetap benar (idempotent).

Seluruh istilah pada section ini — topic, partition, offset, producer, consumer — berakar pada dokumentasi Apache Kafka , platform streaming yang menjadi rujukan industri. Fondasi konsepnya sudah terpasang; pertanyaan praktis berikutnya adalah apa yang sebenarnya berubah antara pipeline batch yang sudah kita kuasai dan pipeline stream. Perbandingan mekanisnya kita ukur langsung pada Chapter Batch vs Stream Processing.