Chapter Batch Processing vs Stream Processing di awal course sudah memetakan perbedaan konseptual kedua gaya; section ini mengambil sudut yang lebih mekanis. Pertanyaannya bukan lagi “kapan hasil dibutuhkan”, melainkan “apa yang berbeda di dalam kode” ketika agregasi yang sama dijalankan dengan dua gaya. Pemicunya nyata: tim marketing TokoKita meminta dashboard kampanye menampilkan total pendapatan per kota tanpa menunggu rekap pukul 06:00. Sebelum memutuskan, kita jalankan kedua gaya atas data yang persis sama, ukur waktunya, lalu verifikasi hasilnya identik.

Dua Gaya, Satu Hasil yang Sama #

Gaya batch bekerja dalam dua fase: fase tunggu — data dikumpulkan sampai dianggap lengkap — lalu fase proses — agregasi dijalankan sekali jalan atas seluruh kumpulan. Gaya stream melebur dua fase tersebut: setiap event yang tiba langsung memperbarui hasil, sehingga hasil antara selalu tersedia dan tidak pernah ada momen “menunggu data lengkap”. Kedua gaya menghasilkan angka akhir yang sama; yang berbeda adalah kapan angka tersebut mulai bisa dibaca.

AspekGaya batchGaya stream
Fase kerjatunggu lengkap, proses sekali jalanproses tiap event begitu tiba
Hasil antaratidak ada — tabel hasil muncul di akhirsnapshot terbaca kapan saja
Hasil pertama siapsetelah seluruh data terkumpulsetelah event pertama
Pemulihan kegagalanjalankan ulang job atas data samalanjut dari posisi baca terakhir
Bentuk operasionaljob terjadwal (DAG Airflow, job Spark)service yang berjalan terus

Satu hal yang tidak berubah di antara kedua gaya: definisi hasil akhirnya. Total pendapatan per kota dihitung dengan aturan yang sama — jumlahkan nilai seluruh pesanan lalu kelompokkan per kota. Gaya hanya menentukan kapan dan berapa kali perhitungan diperbarui; selama definisi bisnisnya tetap, hasil akhir kedua gaya wajib identik. Prinsip inilah yang membuat perpindahan gaya bisa diverifikasi otomatis: jalankan keduanya atas data yang sama, lalu bandingkan hasilnya.

Eksperimen Terukur dengan Python #

Eksperimen berikut mengagregasi 200.000 pesanan TokoKita menjadi total pendapatan per kota. Generator aliran_pesanan memainkan peran kedatangan event satu per satu; time.perf_counter mengukur setiap fase.

python
"""Agregasi pendapatan per kota, dua gaya: batch (kumpulkan semua lalu proses)
vs stream (hasil diperbarui tiap event). Diverifikasi hasilnya identik."""

import random
import time

KOTA = ["jakarta", "bandung", "surabaya", "medan", "denpasar"]
JUMLAH_EVENT = 200_000


def aliran_pesanan():
    """Generator: event pesanan 'tiba' satu per satu sepanjang hari."""
    acak = random.Random(7)
    for _ in range(JUMLAH_EVENT):
        yield acak.choice(KOTA), acak.randrange(25, 750) * 1000


def gaya_batch():
    """Batch: menunggu seluruh data terkumpul, baru agregasi sekali jalan."""
    mulai = time.perf_counter()
    data = list(aliran_pesanan())  # fase tunggu: data dikumpulkan dulu
    selesai_kumpul = time.perf_counter()

    total = {}
    for kota, nilai in data:  # fase proses: baru dikerjakan setelah lengkap
        jumlah, sum_nilai = total.get(kota, (0, 0))
        total[kota] = (jumlah + 1, sum_nilai + nilai)
    selesai_proses = time.perf_counter()

    print("GAYA BATCH")
    print(f"  menunggu data lengkap      : {selesai_kumpul - mulai:.3f} detik")
    print(f"  agregasi setelah lengkap   : {selesai_proses - selesai_kumpul:.3f} detik")
    print(f"  hasil pertama tersedia setelah {selesai_proses - mulai:.3f} detik\n")
    return total


def gaya_stream():
    """Stream: hasil diperbarui tiap event, siap dibaca kapan saja."""
    total = {}
    mulai = time.perf_counter()
    for nomor, (kota, nilai) in enumerate(aliran_pesanan(), start=1):
        jumlah, sum_nilai = total.get(kota, (0, 0))
        total[kota] = (jumlah + 1, sum_nilai + nilai)
        if nomor in (1, JUMLAH_EVENT // 2):
            print(f"  [stream] setelah {nomor:>7,} event: "
                  f"hasil sementara sudah terbaca (t={time.perf_counter() - mulai:.3f}s)")
    print(f"  [stream] seluruh {JUMLAH_EVENT:,} event terproses "
          f"(t={time.perf_counter() - mulai:.3f}s)\n")
    return total


def main():
    print(f"Agregasi pendapatan {JUMLAH_EVENT:,} pesanan TokoKita per kota:\n")
    hasil_batch = gaya_batch()

    print("GAYA STREAM")
    hasil_stream = gaya_stream()

    print("Verifikasi hasil batch == hasil stream:", hasil_batch == hasil_stream)
    for kota in sorted(hasil_batch):
        jumlah, sum_nilai = hasil_batch[kota]
        print(f"  {kota:<9} {jumlah:>6,} pesanan  Rp{sum_nilai:>13,}")


if __name__ == "__main__":
    main()

Output yang Dihasilkan:

text
Agregasi pendapatan 200,000 pesanan TokoKita per kota:

GAYA BATCH
  menunggu data lengkap      : 0.241 detik
  agregasi setelah lengkap   : 0.037 detik
  hasil pertama tersedia setelah 0.278 detik

GAYA STREAM
  [stream] setelah       1 event: hasil sementara sudah terbaca (t=0.000s)
  [stream] setelah 100,000 event: hasil sementara sudah terbaca (t=0.162s)
  [stream] seluruh 200,000 event terproses (t=0.296s)

Verifikasi hasil batch == hasil stream: True
  bandung   39,949 pesanan  Rp15,501,596,000
  denpasar  39,837 pesanan  Rp15,417,790,000
  jakarta   40,079 pesanan  Rp15,453,523,000
  medan     40,172 pesanan  Rp15,590,759,000
  surabaya  39,963 pesanan  Rp15,497,344,000

Mari kita telaah baris-baris kode di atas:

  1. Baris 11-15 (aliran_pesanan): generator memainkan aliran event; benih Random(7) membuat data eksperimen identik untuk kedua gaya.
  2. Baris 21 (data = list(...)): inilah fase tunggu batch — generator dipaksa habis menjadi list; data “lengkap” baru ada setelah baris tersebut selesai.
  3. Baris 25-27 (loop agregasi batch): agregasi baru dikerjakan setelah seluruh data tersimpan di memori, ciri khas olahan sekali jalan.
  4. Baris 41-43 (loop gaya stream): agregat per kota diperbarui di dalam loop tanpa list penampung — tidak ada momen menunggu data lengkap.
  5. Baris 44-46 (checkpoint): cetakan di tengah jalan membuktikan hasil antara sudah terbaca setelah event pertama dan setelah 100.000 event.
  6. Baris 59 (verifikasi): kedua dictionary dibandingkan — True berarti pindah gaya tidak mengubah angka akhirnya.

Membaca Hasil Eksperimen #

Gaya batch menunjukkan profil dua fase yang jelas: 0,241 detik menunggu data lengkap, lalu 0,037 detik mengagregasi. Hasil pertama baru bisa dibaca setelah 0,278 detik — pada pipeline produksi, “menunggu lengkap” bukan sepersekian detik melainkan jendela hingga pukul 06:00. Gaya stream mencetak hasil sementara pada event pertama (t=0.000s), dan angka tersebut terus diperbarui sampai seluruh 200.000 event terproses pada t=0.296s.

Perhatikan bahwa stream tidak lebih cepat secara total — kedua gaya menyelesaikan seluruh data dalam rentang 0,2-0,3 detik yang kurang lebih sama, dan angka pastinya bergeser antar eksekusi (pada run kedua, fase tunggu batch terukur 0,168 detik). Nilai stream bukan throughput melainkan kesegaran: hasil antara tersedia sejak detik pertama, bukan hanya di akhir. Satu baris Verifikasi ... True menutup kekhawatiran yang paling sering menghentikan migrasi: mengubah gaya pengolahan tidak mengubah angka akhirnya.

Memilih Gaya untuk Kebutuhan yang Benar #

Tiga pertanyaan berikut memandu keputusan di TokoKita:

  • Apakah nilai hasil meluruh seiring waktu? Deteksi penipuan dan sisa stok kehilangan nilai dalam menit — layak stream; rekap komisi penjual tetap cukup batch.
  • Apakah hasil antara sudah bermakna? Total pendapatan berjalan bermakna sejak event pertama; rata-rata yang butuh seluruh populasi tidak.
  • Apakah organisasi siap mengoperasikan service 24 jam? Stream berarti monitoring, pemulihan state, dan penanganan event telat — beban yang tidak ada pada job terjadwal.

Kedua gaya tersebut sama-sama membutuhkan satu hal yang belum kita bahas: jalur bagi event untuk berpindah dari aplikasi ke pipeline secara andal. Pola arsitektur yang menjawab kebutuhan tersebut kita bangun pada Chapter Event-Driven Architecture.