Monolit vs event-driven bukan pertanyaan soal arsitektur mana yang lebih modern, tetapi mana yang paling aman dioperasikan untuk beban, tim, dan risiko bisnis Anda. Pada sistem kritis, keputusan ini memengaruhi cara tim merespons insiden, memahami alur data, mengisolasi kerusakan, dan menjaga throughput saat kondisi tidak ideal.

Pemicu diskusi ini sering datang dari cerita yang terlihat sederhana: proses knowledge extraction berjalan di backend, tiba-tiba ada crash, consumer Kafka melakukan rebalance, throughput turun, sebagian pesan diproses ulang, dan tim kesulitan memastikan status akhir data. Dari sini terlihat bahwa kompleksitas event-driven bukan hanya pada desain, tetapi pada operasi sehari-hari. Di sisi lain, monolit juga bukan jawaban universal: ia bisa lebih mudah dipahami, tetapi bisa memperbesar dampak jika semua alur kritis hidup dalam satu proses dan satu deployment unit.

Masalah yang sebenarnya: bukan sinkron vs asinkron, tetapi biaya koordinasi

Pada banyak diskusi, monolit sering diposisikan sebagai lawan dari microservices, padahal dalam praktik, pembanding yang lebih berguna untuk sistem backend kritis adalah monolit modular versus arsitektur event-driven. Keduanya bisa hidup dalam satu organisasi tanpa harus saling menggantikan total.

Perbedaannya terletak pada lokasi kompleksitas:

  • Monolit modular menaruh kompleksitas terutama di dalam codebase, boundary modul, transaksi database, dan deployment bersama.
  • Event-driven memindahkan sebagian kompleksitas ke komunikasi antarkomponen, broker, skema event, retry, ordering, idempotency, dan observabilitas lintas proses.

Artinya, ketika tim memutuskan memakai Kafka, RabbitMQ, atau queue lain, mereka tidak menghilangkan kompleksitas. Mereka menukar jenis kompleksitas dari yang lebih lokal menjadi lebih terdistribusi.

Monolit modular: kapan justru lebih aman untuk sistem kritis

Monolit modular sering diremehkan karena dianggap kurang skalabel. Padahal untuk banyak sistem kritis, pendekatan ini memberi keuntungan operasional yang sangat besar, terutama ketika kebutuhan konsistensi tinggi dan tim masih ingin mempertahankan alur diagnosis yang sederhana.

Kelebihan operasional monolit modular

  • Alur eksekusi mudah ditelusuri. Satu request dapat diikuti dari controller ke service ke database tanpa harus melintasi broker dan consumer terpisah.
  • Debugging lebih langsung. Log, stack trace, dan state biasanya terkumpul di satu tempat.
  • Transaksi lebih sederhana. Banyak kebutuhan bisnis dapat diselesaikan dengan transaksi database lokal tanpa pola distributed consistency.
  • Pengetahuan sistem lebih terpusat. Engineer baru lebih cepat memahami alur inti.
  • Operasional lebih murah. Tidak perlu cluster broker, schema registry, dead-letter flow, dan observabilitas lintas komponen sejak awal.

Keterbatasan monolit modular

  • Blast radius deployment lebih besar jika boundary modul lemah atau semua fitur berbagi resource yang sama.
  • Skalabilitas per-komponen lebih kasar. Anda cenderung menskalakan seluruh aplikasi, bukan hanya bagian yang panas.
  • Kode bisa membesar tanpa disiplin modularisasi, lalu berubah menjadi “big ball of mud”.
  • Antrian pekerjaan internal tetap dibutuhkan untuk task berat, sehingga monolit murni sinkron jarang cukup untuk beban nyata.

Karena itu, monolit yang relevan untuk sistem kritis bukan monolit tanpa struktur, melainkan modular monolith: boundary domain jelas, kontrak internal rapi, side effect dibatasi, dan pekerjaan berat bisa diproses asinkron secara terkontrol.

Event-driven: kuat untuk isolasi beban, tetapi mahal secara operasional

Arsitektur event-driven unggul ketika beban tidak merata, ada banyak proses turunan, atau sistem harus tetap responsif walau sebagian pekerjaan dapat ditunda. Contohnya, saat request pengguna hanya perlu menyimpan data awal, lalu knowledge extraction, indexing, enrichment, notification, dan audit berjalan di background.

Keuntungan utama event-driven

  • Decoupling waktu. Producer tidak harus menunggu seluruh proses selesai.
  • Skalabilitas lebih terarah. Consumer untuk pekerjaan berat bisa diskalakan terpisah.
  • Isolasi beban. Lonjakan traffic pada satu alur tidak selalu memblokir semua fitur.
  • Ekstensi lebih mudah. Subscriber baru dapat ditambahkan tanpa mengubah request path utama, selama kontrak event stabil.

Biaya yang sering diremehkan

  • Debugging menjadi lintas proses. Satu kejadian bisnis bisa melibatkan producer, broker, beberapa consumer, retry worker, dan kompensasi.
  • At-least-once delivery berarti duplikasi harus dianggap normal, bukan edge case.
  • Ordering tidak gratis. Banyak tim salah mengasumsikan pesan akan selalu diproses sesuai urutan bisnis yang mereka harapkan.
  • Rebalance, lag, poison message, dan backpressure menjadi masalah operasional rutin.
  • Ownership pengetahuan menyebar. Tidak ada satu tim yang benar-benar paham seluruh rantai jika dokumentasi dan observabilitas lemah.

Dalam sistem kritis, ini berarti event-driven baru layak ketika tim siap mengelola broker sebagai bagian dari sistem produksi, bukan sekadar “alat kirim pesan”.

Konteks nyata: crash, knowledge extraction, dan rebalance Kafka

Bayangkan alur backend berikut:

  1. Aplikasi menerima dokumen dari pengguna.
  2. Metadata disimpan ke database.
  3. Sebuah event DocumentUploaded dipublikasikan.
  4. Consumer knowledge-extraction membaca event, mengekstrak isi, membuat embedding atau indeks, lalu menyimpan hasilnya.

Masalah muncul saat proses extraction cukup berat. Jika consumer membutuhkan waktu lama untuk satu pesan, lalu proses crash di tengah jalan atau tidak bisa melakukan polling sesuai ekspektasi group coordination, maka rebalance dapat terjadi. Partisi dipindahkan ke consumer lain. Akibatnya:

  • Throughput turun sementara karena konsumer berhenti sejenak saat rebalance.
  • Pesan yang belum sempat di-commit bisa dibaca ulang.
  • Jika operasi downstream tidak idempotent, hasil extraction bisa ganda atau status data menjadi ambigu.
  • Tim sulit menjawab pertanyaan sederhana: “dokumen ini sebenarnya sudah diproses atau belum?”

Ini bukan bug Kafka semata. Ini konsekuensi desain sistem yang belum cukup eksplisit soal idempotency, checkpoint, offset commit strategy, dan pemisahan tugas berat.

Skenario insiden rebalance yang realistis

Misalkan ada consumer group knowledge-extractor dengan tiga instance. Setiap pesan memicu proses OCR dan parsing yang kadang memakan waktu lama. Salah satu instance mengalami crash setelah berhasil menyimpan separuh hasil extraction, tetapi sebelum offset pesan di-commit.

Lalu urutannya menjadi seperti ini:

  1. Broker mendeteksi anggota group hilang atau tidak lagi dianggap sehat.
  2. Consumer group melakukan rebalance.
  3. Partisi yang sebelumnya dimiliki instance yang crash diberikan ke instance lain.
  4. Pesan terakhir dibaca ulang karena offset belum dianggap selesai.
  5. Consumer baru memproses kembali dokumen yang sama.

Jika penyimpanan hasil extraction hanya memakai operasi insert biasa, Anda bisa mendapat:

  • duplikasi record hasil extraction,
  • status dokumen yang meloncat dari processing ke done dua kali,
  • notifikasi ganda ke sistem lain,
  • lag meningkat karena pekerjaan mahal diproses ulang.

Di sinilah event-driven menuntut disiplin engineering yang lebih tinggi dibanding monolit sinkron biasa.

Pola mitigasi yang sebaiknya ada

  • Idempotency key berbasis document ID + tahap proses.
  • Upsert atau conditional write pada penyimpanan hasil.
  • Status machine yang eksplisit, misalnya queued, processing, processed, failed.
  • Commit offset hanya setelah efek samping penting selesai, sambil menerima bahwa ini meningkatkan peluang redelivery jika crash terjadi sebelum commit.
  • Pisahkan kerja CPU-bound/IO-bound berat dari loop consumer utama jika perlu, agar koordinasi consumer tetap sehat.
  • Retry terbatas dan dead-letter policy untuk pesan yang berulang kali gagal.

Contoh pseudocode consumer yang lebih aman

for message in consume():
    event = parse(message)
    key = "extract:" + event.document_id

    if already_processed(key):
        commit(message)
        continue

    mark_processing_if_absent(key)

    try:
        result = run_extraction(event)
        save_result_upsert(event.document_id, result)
        mark_processed(key)
        commit(message)
    except TemporaryError:
        # jangan tandai processed; biarkan retry/redelivery terjadi
        rollback_local_state_if_needed()
        raise
    except PermanentError:
        send_to_dead_letter(message)
        mark_failed(key)
        commit(message)

Contoh di atas bukan resep universal, tetapi menunjukkan prinsip penting: pesan bisa datang lagi, jadi operasi harus dirancang aman terhadap pengulangan.

Perbandingan operasional: monolit modular vs event-driven

1. Skalabilitas

Monolit modular cocok jika bottleneck masih bisa diatasi dengan optimasi query, caching, batching, background job sederhana, atau scale-up/scale-out aplikasi secara umum. Jika 80% beban berasal dari beberapa endpoint yang masih berbagi state kuat dengan domain lain, memecah terlalu cepat justru menambah koordinasi.

Event-driven layak saat ada alur yang benar-benar independen dalam waktu prosesnya, misalnya ingestion cepat tetapi pemrosesan lanjut mahal. Dengan queue atau Kafka, Anda bisa menyerap lonjakan dan menskalakan consumer khusus. Namun, skalabilitas ini datang dengan biaya delay, ordering issue, dan kebutuhan observabilitas yang lebih matang.

2. Biaya operasional

Monolit modular umumnya lebih murah di fase awal dan menengah. Komponen yang dijalankan lebih sedikit, pipeline deployment lebih sederhana, dan kebutuhan on-call lebih ringan.

Event-driven menambah biaya pada:

  • broker dan storage pesan,
  • monitoring lag, throughput, retry, dead-letter,
  • schema governance untuk event,
  • capacity planning partisi dan consumer concurrency,
  • runbook insiden yang lebih kompleks.

Jika tim belum punya pengalaman mengoperasikan message broker, biaya belajar sering lebih besar daripada biaya infrastrukturnya sendiri.

3. Kompleksitas debugging

Monolit modular menang telak untuk kasus di mana satu transaksi bisnis perlu ditelusuri cepat saat insiden. Anda bisa menghubungkan request ID, query database, dan log aplikasi secara langsung.

Event-driven menuntut correlation ID, distributed tracing, log terstruktur, dan visualisasi aliran event. Tanpa itu, debugging berubah menjadi pencarian manual di banyak log dan dashboard.

Kesalahan umum adalah mengadopsi event-driven tanpa correlation ID yang konsisten. Akibatnya, tim tahu pesan masuk ke broker, tetapi tidak tahu jejak bisnisnya berakhir di mana.

4. Observabilitas

Pada monolit, observabilitas fokus pada metrik klasik: latency request, error rate, CPU, memory, query lambat, dan job queue internal.

Pada event-driven, observabilitas harus mencakup:

  • consumer lag per topic/partition/group,
  • throughput publish dan consume,
  • success/failure rate per jenis event,
  • umur pesan di antrian,
  • jumlah retry dan dead-letter,
  • waktu end-to-end dari event dipublikasikan hingga efek bisnis selesai.

Tanpa metrik ini, Anda hanya melihat gejala akhir, bukan lokasi bottleneck.

5. Ownership pengetahuan

Monolit modular memudahkan shared understanding karena logika inti tinggal dalam satu repo atau satu service utama. Ini penting untuk sistem kritis yang butuh rotasi on-call lintas tim.

Event-driven sering membagi ownership dengan baik secara organisasi, tetapi juga menciptakan risiko fragmentasi pengetahuan. Tim A paham producer, tim B paham consumer, tim C paham pipeline data, tetapi tidak ada yang benar-benar memegang alur ujung ke ujung saat insiden besar terjadi.

6. Blast radius insiden

Monolit modular punya blast radius deployment yang lebih besar jika isolasi internal lemah. Satu memory leak atau deadlock bisa memengaruhi banyak fitur sekaligus.

Event-driven bisa memperkecil dampak sinkron langsung, tetapi dapat memperbesar blast radius data. Misalnya satu event rusak atau satu consumer bug bisa menyebarkan status salah ke banyak downstream. Efeknya tidak selalu langsung terlihat, tetapi lebih sulit dipulihkan.

7. Maintainability tim

Maintainability bukan hanya soal kode mudah dibaca, tetapi apakah tim mampu mengubah sistem tanpa meningkatkan risiko operasi secara tidak proporsional.

Monolit modular lebih maintainable jika tim kecil-menengah, domain masih berkembang, dan boundary belum stabil.

Event-driven lebih maintainable jika boundary domain sudah cukup matang, tim dapat memiliki kontrak event dengan disiplin, dan perubahan satu alur memang perlu independen dari alur lain.

Kapan Kafka atau queue benar-benar layak dipakai

Queue atau broker event layak dipakai jika beberapa kondisi ini nyata, bukan sekadar prediksi:

  • Pekerjaan berat tidak boleh menahan request utama, dan hasil boleh konsisten secara eventual.
  • Ada lonjakan traffic yang perlu diserap agar sistem tidak langsung jatuh saat spike.
  • Beberapa consumer berbeda membutuhkan event yang sama untuk tujuan yang berbeda.
  • Backpressure perlu dikelola secara eksplisit.
  • Tim siap membangun idempotency, retry, dead-letter, dan observabilitas end-to-end.
  • Kegagalan downstream tidak boleh langsung menjatuhkan alur utama.

Kafka khususnya lebih masuk akal ketika volume event tinggi, throughput penting, dan kebutuhan partisi/consumer group benar-benar berguna untuk pola kerja Anda. Jika kebutuhan hanya job background sederhana, alat yang lebih kecil bisa lebih cocok daripada langsung memakai platform event yang kompleks.

Kapan event-driven terlalu dini

  • Tim masih kesulitan menjaga boundary domain di dalam satu codebase.
  • Masalah utama sebenarnya query lambat, locking database, atau desain data yang buruk.
  • Alur bisnis butuh transaksi kuat dan jawaban sinkron dalam satu tempat.
  • Belum ada kemampuan observabilitas yang memadai.
  • Belum ada engineer yang nyaman mengoperasikan broker, menangani redelivery, dan men-debug masalah konsistensi eventual.
  • Use case hanya “ingin lebih scalable” tanpa bottleneck konkret.

Sering kali langkah yang lebih tepat adalah memperbaiki monolit menjadi modular, menambahkan worker background terbatas, lalu mengukur bottleneck nyata sebelum migrasi besar.

Anti-pattern umum

Pada monolit modular

  • Modular hanya di folder, tetapi semua modul bebas mengakses tabel dan service modul lain.
  • Semua proses dibuat sinkron meski ada pekerjaan berat yang jelas lebih cocok di background.
  • Satu database transaction untuk operasi panjang sehingga lock dan contention meningkat.

Pada event-driven

  • Menganggap event sebagai RPC terselubung. Producer mengharapkan efek langsung dan pasti, padahal sistem bersifat eventual.
  • Tidak merancang idempotency karena berasumsi pesan hanya datang sekali.
  • Event schema berubah tanpa governance sehingga consumer lama rusak diam-diam.
  • Melepas terlalu banyak event granular yang sulit dipahami secara bisnis.
  • Menggunakan Kafka untuk semuanya, termasuk pekerjaan sederhana yang cukup dengan tabel job atau queue biasa.
  • Retry tak terbatas tanpa dead-letter, sehingga pesan beracun terus memakan resource.

Tabel keputusan singkat

KriteriaMonolit ModularEvent-Driven
Kebutuhan konsistensi langsungSangat cocokLebih sulit, perlu desain khusus
Lonjakan beban dan bufferingTerbatasSangat cocok
Kemudahan debugging insidenLebih mudahLebih sulit tanpa tracing matang
Biaya operasional awalLebih rendahLebih tinggi
Skalabilitas per alur kerjaLebih kasarLebih fleksibel
Risiko duplikasi/ordering issueLebih rendahHarus dianggap desain inti
Kecocokan untuk tim kecilTinggiRendah sampai sedang
Isolasi kegagalan antar prosesTerbatasLebih baik, tetapi data flow lebih kompleks

Checklist evaluasi sebelum migrasi ke event-driven

  1. Apakah bottleneck sudah terukur? Sebutkan endpoint, job, query, atau proses yang benar-benar menjadi masalah.
  2. Apakah eventual consistency bisa diterima bisnis? Jika tidak, event-driven penuh mungkin bukan pilihan utama.
  3. Apakah setiap consumer bisa dibuat idempotent? Jika tidak, risiko operasi meningkat tajam.
  4. Apakah ada correlation ID end-to-end?
  5. Apakah tim punya dashboard untuk lag, retry, dan dead-letter?
  6. Apakah skema event punya versi atau kompatibilitas yang jelas?
  7. Apakah ada runbook untuk rebalance, consumer crash, backlog, dan poison message?
  8. Apakah ownership alur bisnis jelas? Satu insiden tidak boleh membutuhkan lima tim hanya untuk menentukan sumber masalah.
  9. Apakah pekerjaan berat sudah dipisahkan dari lifecycle consumer?
  10. Apakah rollback bisnis dipahami? Di sistem event-driven, rollback jarang sesederhana membatalkan satu transaksi.

Pendekatan praktis yang sering paling masuk akal

Untuk banyak tim, pilihan terbaik bukan ekstrem monolit murni atau event-driven penuh, melainkan:

  • Mulai dari monolit modular dengan boundary domain yang jelas.
  • Gunakan queue secara terbatas untuk pekerjaan berat yang tidak harus sinkron, seperti knowledge extraction, thumbnailing, indexing, atau notifikasi.
  • Promosikan alur menjadi event-driven hanya ketika bottleneck, ownership, dan pola konsumennya memang membenarkan biaya tambahan.

Pendekatan ini menahan kompleksitas agar muncul hanya ketika ada alasan operasional yang kuat. Anda mendapatkan manfaat asinkron pada titik yang tepat tanpa memaksa seluruh sistem menanggung beban distribusi sejak dini.

Kesimpulan

Monolit vs event-driven untuk sistem kritis pada dasarnya adalah keputusan tentang trade-off operasional. Monolit modular unggul dalam kesederhanaan diagnosis, biaya yang lebih rendah, dan shared understanding tim. Event-driven unggul saat perlu menyerap lonjakan beban, mengisolasi alur kerja mahal, dan menskalakan proses secara independen.

Tetapi event-driven baru benar-benar layak jika tim siap menghadapi konsekuensi nyata: rebalance, duplikasi pesan, eventual consistency, observabilitas lintas komponen, dan ownership pengetahuan yang tersebar. Jika belum siap, migrasi terlalu cepat justru mengubah masalah yang bisa diperbaiki di codebase menjadi insiden produksi yang lebih sulit dipahami.

Untuk backend kritis, pertanyaan yang paling sehat bukan “arsitektur mana yang paling keren?”, melainkan: arsitektur mana yang membuat tim kami paling mampu menjaga sistem tetap benar, terukur, dan pulih saat gagal?