Lambda architecture pada chapter sebelumnya menyelesaikan pertukaran kesegaran dan keakuratan, tetapi memindahkannya ke kompleksitas: satu definisi bisnis hidup di dua codebase. Jay Kreps, salah satu pencipta Apache Kafka, mengajukan penyederhanaan yang sekarang dikenal sebagai Kappa architecture: singkirkan jalur batch, simpan log event cukup lama, dan lakukan reprocessing dengan memutar ulang log dari awal lewat versi job baru. Satu codebase, satu jalur, dan log yang sama berfungsi ganda sebagai sumber real-time sekaligus arsip. Bab ini mensimulasikan mekanisme tersebut dan menunjukkan syarat yang membuatnya sah: log harus bisa di-replay.
Satu Jalur: Log dan Replay #
Inti Kappa adalah keyakinan bahwa log append-only sudah cukup sebagai satu-satunya sumber kebenaran. Job streaming membaca log secara kontinu dan menyimpan hasilnya ke serving table; ketika logika pemrosesan perlu diubah, kita tidak menyentuh job lama. Sebagai gantinya, versi baru job membaca ulang log dari offset 0 ke tabel output baru, pembaca berpindah setelah hasilnya siap, dan job lama dihentikan.
Simulasi Replay dengan DuckDB #
Kita mainkan peran Kafka topic dengan tabel DuckDB bernama event_log yang memiliki kolom log_offset monoton. Enam event pertama sudah berada di dalam log, dan dua tabel output disiapkan berdampingan:
import duckdb
con = duckdb.connect()
# --- Log append-only: hanya INSERT, tidak ada UPDATE/DELETE ---
con.execute("""
CREATE TABLE event_log (
log_offset INTEGER, event_id VARCHAR, kota VARCHAR,
status VARCHAR, nilai BIGINT
)
""")
con.executemany("INSERT INTO event_log VALUES (?, ?, ?, ?, ?)", [
(1, "e-201", "jakarta", "lunas", 150000),
(2, "e-202", "bandung", "lunas", 90000),
(3, "e-203", "jakarta", "batal", 60000),
(4, "e-204", "surabaya", "lunas", 120000),
(5, "e-205", "bandung", "lunas", 75000),
(6, "e-206", "jakarta", "lunas", 200000),
])
con.execute("CREATE TABLE view_v1 (kota VARCHAR, pendapatan BIGINT, jumlah_event BIGINT)")
con.execute("CREATE TABLE view_v2 (kota VARCHAR, pendapatan BIGINT, jumlah_event BIGINT)")Satu-satunya fungsi pemrosesan melayani kedua versi logika lewat parameter — perhatikan bahwa log tidak pernah diubah:
def jalankan_job(tabel_view, hanya_lunas, dari_offset):
"""Proses event offset > dari_offset, lalu gabungkan ke tabel view."""
filter_status = "AND status = 'lunas'" if hanya_lunas else ""
con.execute(f"""
INSERT INTO {tabel_view}
SELECT kota, sum(nilai), count(*)
FROM event_log
WHERE log_offset > {dari_offset} {filter_status}
GROUP BY kota
""")
con.execute(f"""
CREATE OR REPLACE TABLE {tabel_view} AS
SELECT kota, sum(pendapatan) AS pendapatan,
sum(jumlah_event) AS jumlah_event
FROM {tabel_view} GROUP BY kota
""")
ujung = con.execute("SELECT max(log_offset) FROM event_log").fetchone()[0]
print(f" job memproses offset {dari_offset + 1}-{ujung}")
def tampil(tabel_view):
return con.execute(
f"SELECT * FROM {tabel_view} ORDER BY kota").fetchall()Skenarionya: job v1 menghitung semua status sebagai pendapatan — bug yang belum disadari. Setelah bug diketahui, job v2 me-replay seluruh log dengan logika yang hanya menerima status lunas, lalu dua event baru (offset 7-8) tiba dan diproses kedua job. Baris pemanggilan skenario dipangkas dari tampilan; hasil lengkapnya:
Output yang Dihasilkan:
== Job v1: logika lama (semua status dihitung sebagai pendapatan) ==
job memproses offset 1-6
bandung 2 event Rp165,000
jakarta 3 event Rp410,000
surabaya 1 event Rp120,000
-> jakarta termasuk e-203 berstatus batal (Rp60.000): salah tapi belum diketahui
== Bug ditemukan: pesanan batal ikut terhitung sebagai pendapatan ==
Job v2 me-replay log dari offset 0 dengan logika baru (hanya lunas);
view_v1 tetap melayani pembaca selama replay berlangsung
job memproses offset 1-6
bandung 2 event Rp165,000
jakarta 2 event Rp350,000
surabaya 1 event Rp120,000
== Event baru offset 7-8 tiba; kedua job memproses rentang yang sama ==
job memproses offset 7-8
job memproses offset 7-8
== Perbandingan akhir ==
kota v1 (keliru) v2 (benar)
bandung 250,000 165,000
jakarta 410,000 350,000
surabaya 215,000 215,000
selisih = nilai event batal: 145000
pembaca berpindah ke view_v2; view_v1 dihapus
log tetap utuh, hanya pernah INSERT: 8 event, offset 1-8, tidak ada baris yang diubahMari kita telaah baris-baris kode di atas:
- Blok pertama, baris 6-19 (
event_log): log append-only denganlog_offsetyang selalu meningkat; seluruh skenario hanya memakaiINSERT, tidak adaUPDATEatauDELETEatas baris lama. - Blok pertama, baris 20-21 (
view_v1danview_v2): dua tabel output hidup berdampingan; pembaca lama tetap dilayaniview_v1selamaview_v2dibangun oleh replay. - Blok kedua, baris 3 (
filter_status): perbaikan logika berupa satu parameter — log tidak berubah, yang berubah hanyalah cara job membacanya. - Blok kedua, baris 4-16 (
jalankan_job): job memproses event denganlog_offset > dari_offsetlalu menggabungkan hasilnya ke tabel view; replay hanyalah pemanggilan yang sama dengandari_offset=0, seperti terlihat pada baris outputjob memproses offset 1-6yang muncul dua kali.
Perbandingan akhir pada output menunjukkan selisih Rp145.000 — tepat sebesar nilai event batal yang ikut terhitung di v1. Setelah pembaca berpindah, view_v1 dihapus dan log tetap utuh.
Syarat Replay: Retensi dan Compaction #
Kappa hanya sah bila log benar-benar bisa diputar ulang. Kafka menyediakan dua mekanisme yang perlu dipertimbangkan: retensi berbasis waktu atau ukuran (log dipertahankan beberapa hari sampai bertahun-tahun, dengan biaya storage yang mengikuti), dan log compaction (untuk topic berbasis kunci, hanya nilai terbaru per kunci yang dipertahankan).
Lambda atau Kappa #
| Aspek | Lambda Architecture | Kappa Architecture |
|---|---|---|
| Jalur pemrosesan | batch + speed | satu jalur stream |
| Reprocessing | batch layer recompute | replay log dengan job versi baru |
| Jumlah codebase | dua, wajib setara | satu |
| Prasyarat utama | data tersedia utuh untuk batch | log retensi panjang dan bisa di-replay |
| Cocok saat | event telat banyak, recompute murah | logika sering berubah, log terkelola |
Dokumentasi resmi mekanisme log, retensi, dan compaction yang menjadi fondasi Kappa tersedia di kafka.apache.org .
Sejauh ini seluruh arsitektur kita bergerak pada satu sumbu: kecepatan dan kebenaran pemrosesan. Pertanyaan berikutnya berpindah ke dimensi lain — organisasi. Ketika puluhan domain bisnis dan banyak tim terlibat, siapa yang sebaiknya memiliki datanya? Chapter Data Mesh menjawab dengan membalik arsitektur terpusat.