Event-Driven Architecture: Kapan Sebaiknya TIDAK Dipakai

Foto oleh Josh Sorenson

Foto oleh Josh Sorenson
Event-driven architecture adalah pola yang semua orang ingin cantumkan di CV tapi hampir tidak pernah dipertanyakan. Janjinya menggoda: service yang decoupled, skalabilitas tanpa batas, sistem yang bereaksi terhadap dunia alih-alih melakukan polling. Saya sudah merilis alur event-driven di production dan akan terus melakukannya. Tapi saya juga pernah melihat tim, termasuk diri saya di masa lalu, mengambil sistem CRUD yang mudah dipahami lalu mengubahnya menjadi misteri pembunuhan terdistribusi di mana tidak ada yang bisa menjawab pertanyaan operasional paling sederhana: apa yang terjadi, dalam urutan apa, dan kenapa.
Tulisan ini adalah kerangka keputusan yang dulu saya harap ada yang memberikannya sebelum saya menyambungkan message broker pertama ke sebuah ERP. Jawaban singkatnya di awal: kalau sistem Anda muat di satu database, tim Anda muat di satu ruangan, dan workflow Anda butuh jawaban sekarang bukan nanti, kemungkinan besar Anda belum butuh event. Begini cara memastikannya.
Mari adil dulu terhadap polanya. Tulisan klasik Martin Fowler membedakan event notification, event-carried state transfer, event sourcing, dan CQRS, dan masing-masing menyelesaikan masalah coupling yang spesifik. Event notification membuat producer bisa mengumumkan bahwa sesuatu terjadi tanpa perlu tahu siapa yang mendengarkan. Event-carried state transfer membuat consumer menyimpan salinan datanya sendiri sehingga bisa menjawab query tanpa memanggil sistem sumber. Ini properti yang nyata dan berharga ketika Anda punya banyak tim dan banyak service yang saling bersinggungan.
AWS membingkai ide yang sama secara operasional: producer, router, dan consumer yang scale, gagal, dan deploy secara independen. Kalau pipeline pemrosesan gambar Anda kena lonjakan trafik 100x sementara service billing tidak, menaruh queue di antara keduanya jelas benar. Pertanyaannya tidak pernah apakah event itu berfungsi. Pertanyaannya adalah apakah masalah yang diselesaikannya memang masalah yang Anda hadapi hari ini.
Setiap pola arsitektur adalah pertukaran. Inilah yang Anda bayar untuk event, dan Anda membayarnya sejak hari pertama, bukan saat scale.
Debugging jadi arkeologi
Panggilan synchronous memberi Anda stack trace. Event memberi Anda correlation ID, itu pun kalau Anda ingat mempropagasikannya, tersebar di tiga service dan satu broker. Median time-to-diagnosis insiden production Anda naik sejak hari pertama go async, dan tidak pernah benar-benar turun lagi.
Eventual consistency bocor ke UX
Pengguna tidak berpikir dalam eventual consistency. Mereka klik approve, refresh halaman, dan berharap dunia sudah berubah. Setiap batas async yang Anda tambahkan adalah tempat UI bisa menampilkan data basi, dan Anda akan menghabiskan waktu engineering sungguhan menutupinya dengan optimistic update dan polling.
Penanganan kegagalan berlipat ganda
Kegagalan synchronous adalah satu jalur kode: panggilan gagal, tampilkan error. Kegagalan async adalah kebun binatang: redelivery, pengiriman tidak berurutan, pengiriman duplikat, poison message, dead-letter queue, consumer yang berhenti diam-diam. Masing-masing butuh desain, kode, dan monitoring.
Beban operasional membengkak
Broker adalah satu lagi stateful service yang harus di-deploy, di-patch, dipantau, dan di-backup. Untuk tim kecil dengan budget VPS, Kafka atau bahkan managed queue adalah pajak perhatian permanen yang tidak dikenakan oleh sebuah tabel Postgres.
Dari pengalaman saya membangun sistem NestJS dan PostgreSQL untuk SMB Indonesia, ini bendera merah bahwa desain event-driven justru akan mengurangi nilai:
Versi termahal dari kesalahan ini adalah event sourcing sebagai system of record. Kalau Anda mengadopsinya tanpa kebutuhan keras seperti mandat audit penuh lewat replay, Anda mendaftar untuk evolusi skema atas sejarah yang immutable. Membalikkan keputusan itu nanti berarti proyek migrasi data, bukan refactor.
Ini tabel yang saya gambar di whiteboard saat tim sedang memutuskan. Tidak ada kolom yang lebih baik; keduanya tagihan yang berbeda.
| Dimensi | Synchronous (REST/transaksi) | Event-driven (broker) |
|---|---|---|
| Konsistensi | Langsung, transaksional di tempat yang penting | Eventual; UX dan reporting harus mentolerir jeda |
| Debugging | Satu stack trace, satu log stream | Correlation ID lintas service, dashboard broker, tooling replay |
| Mode kegagalan | Panggilan gagal, pemanggil memutuskan; timeout berantai saat beban tinggi | Retry, duplikat, urutan, poison message, consumer mati diam-diam |
| Coupling antar tim | Pemanggil harus tahu API dan uptime yang dipanggil | Tim rilis independen; kontrak pindah ke skema event |
| Biaya ops untuk tim kecil | Sebatas biaya aplikasi yang sudah ada | Hosting broker, monitoring, upgrade, plus pengetahuan on-call |
Di salah satu proyek ERP, approval purchase order awalnya menerbitkan event POApproved yang diambil consumer inventory untuk mereservasi stok. Di atas kertas, decoupling yang indah. Praktiknya, kedua service dipelihara tiga engineer yang sama dan di-deploy dari monorepo yang sama. Saat payload cacat membuat consumer crash, retry menunda reservasi stok 40 detik sementara staf procurement menatap stok bebas yang seharusnya sudah terkunci.
Kami mengganti event itu dengan satu transaksi database yang meng-update purchase order dan reservasi stok sekaligus. Diagram di bawah adalah kondisi sebelum dan sesudah. Satu kelas insiden terhapus, latency hilang, dan kode menyusut sepertiga. Kami mempertahankan event di tempat yang memang layak: notifikasi ke warehouse reporting dan pengiriman email, di mana jeda 40 detik tidak merugikan apa pun.
-- What "eventually consistent" looked like in practice:
-- the PO was approved, but stock reservation lagged 40s behind
-- because a consumer was retrying a poison message.
Approval service Inventory service User sees
──────────────── ───────────────── ─────────
10:00:00 PO approved
10:00:00 emit POApproved
10:00:01 crash (bad payload)
10:00:05 retry 1 → crash
10:00:15 retry 2 → crash
10:00:40 retry 3 → ok "Why is stock
10:00:40 reserve stock still free?!"
-- The synchronous version is one transaction:
BEGIN;
UPDATE purchase_orders SET status = 'approved' WHERE id = $1;
UPDATE stock_items SET reserved = reserved + qty WHERE ...;
COMMIT; -- consistent at 10:00:00, every timeSebelum queue atau topic mana pun masuk desain saya, ia harus lolos lima pertanyaan ini, berurutan:
Mulailah dengan transactional outbox di atas PostgreSQL sebelum meraih broker. Tabel jobs plus worker polling memberi Anda pemrosesan async, retry, dan tetap hanya satu database yang dioperasikan. Anda bisa naik kelas ke BullMQ atau Kafka nanti dengan mengganti transport, karena pola outbox sudah memaksa Anda mendesain handler yang idempotent.
Supaya tulisan ini bukan serangan sepihak: saya menjalankan alur event-driven dengan senang hati di production hari ini, dan mereka layak dipertahankan di tiga tempat.
Event-driven architecture adalah alat untuk mengelola kompleksitas organisasi dan profil beban, bukan lencana kedewasaan. Sistem yang paling saya banggakan dimulai sebagai monolith dengan satu database PostgreSQL, menambah transactional outbox saat pekerjaan async muncul, dan baru memperkenalkan broker sungguhan ketika kebutuhan fan-out atau load-leveling konkret datang membawa angka.
Intinya: setiap batas async yang Anda tambahkan harus membayar sewa berupa masalah decoupling atau scaling nyata yang ia selesaikan. Kalau tidak bisa, biarkan panggilan tetap synchronous, pertahankan transaksinya, dan nikmati kemampuan membaca perilaku sistem Anda dari satu stack trace. Diri Anda yang jam 2 pagi akan berterima kasih.
Sumber dan bacaan lanjutan