Event-Driven Architecture #
Dalam desain arsitektur perangkat lunak tradisional, komunikasi antar-layanan biasanya dibangun menggunakan model sinkron yang disebut Request-Response (misalnya REST API menggunakan protokol HTTP atau gRPC). Pada model ini, sebuah layanan (Client) mengirimkan permintaan ke layanan lain (Server) dan terpaksa memblokir eksekusinya (blocking) untuk menunggu jawaban sebelum dapat melanjutkan pekerjaannya. Sebaliknya, pada Event-Driven Architecture (EDA) atau Arsitektur Berbasis Event, komunikasi berlangsung secara asinkron. Komponen-komponen sistem berinteraksi dengan cara menerbitkan (publish) dan bereaksi terhadap rekaman fakta perubahan status sistem yang disebut Event. Pengirim event tidak perlu tahu siapa yang akan menerima event tersebut, dan penerima event tidak perlu peduli siapa yang mengirimkannya. Pola ini memutus ketergantungan erat (tight coupling) antar-layanan dan membuka jalan bagi tingkat skalabilitas serta resiliensi sistem yang sangat tinggi di cloud.
Konsep Dasar: Apa itu Event? #
Di dalam rekayasa perangkat lunak, Event didefinisikan sebagai catatan kejadian atau pernyataan fakta yang menerangkan bahwa sesuatu telah terjadi di masa lalu di dalam sistem kita. Karena merekam kejadian masa lalu, event bersifat immutable (tidak dapat diubah atau ditarik kembali) dan biasanya dinamai menggunakan kata kerja bentuk lampau (past-tense verb).
- Contoh Event:
OrderCreated(Pesanan Dibuat),PaymentReceived(Pembayaran Diterima),InvoiceGenerated(Faktur Dibuat), atauUserRegistered(Pengguna Terdaftar). - Isi Payload Event: Memuat informasi penting mengenai kejadian tersebut, seperti ID transaksi, timestamp, dan ringkasan data yang berubah.
sequenceDiagram
participant P as Producer (Layanan Order)
participant B as Event Broker (Kafka/RabbitMQ)
participant C as Consumer (Layanan Inventory)
P->>B: Kirim Event "OrderCreated" (Asinkron)
Note over P: Bebas memproses order berikutnya<br/>(Tanpa Menunggu Inventory)
B-->>C: Teruskan Event "OrderCreated"
Note over C: Kurangi stok produk secara asinkron
Tiga Pola Desain Event: Thin vs Fat vs Event Sourcing #
Di dalam implementasi Event-Driven Architecture, kita harus memilih bagaimana struktur data event dikirimkan dan dikelola. Terdapat tiga pola desain event yang paling umum digunakan di industri:
1. Event Notification (Thin Event) #
Dalam pola ini, event yang dikirimkan berukuran sangat kecil (thin event) dan hanya berisi notifikasi perubahan mendasar serta ID entitas yang berubah.
- Payload:
{"eventId": "evt-123", "type": "OrderCreated", "orderId": "ord-999"} - Kelebihan: Bandwidth jaringan yang dikonsumsi sangat kecil.
- Kekurangan: Penerima event terpaksa melakukan panggilan API balik (query callback) ke layanan pengirim untuk meminta detail data pesanan sebelum dapat memprosesnya. Hal ini memicu ketergantungan API (runtime dependency) yang erat di antara kedua layanan.
2. Event-Carried State Transfer (Fat Event / ECST) #
Pola ini memecahkan masalah panggilan balik dengan mengirimkan data yang lengkap (fat event) di dalam payload event.
- Payload: Memuat data produk yang dibeli, harga, alamat pengiriman, metode pembayaran, dan detail pelanggan.
- Kelebihan: Penerima event memiliki seluruh data yang dibutuhkan untuk memproses transaksi secara mandiri tanpa perlu menanyakan apa pun kembali ke producer.
- Kekurangan: Payload berukuran besar dan membutuhkan bandwidth jaringan yang lebih tinggi. Perubahan skema data juga lebih berisiko merusak kompatibilitas sisi penerima.
3. Event Sourcing #
Pola arsitektur tingkat lanjut di mana status akhir (current state) dari suatu aplikasi tidak disimpan secara langsung di database. Sebaliknya, aplikasi menyimpan seluruh rangkaian perubahan status historis secara berurutan dalam bentuk aliran event (event stream).
- Mekanisme: Untuk mengetahui status saldo rekening nasabah saat ini, sistem akan memutar ulang (replay) seluruh event transaksi debit dan kredit sejak rekening tersebut pertama kali dibuka.
- Kelebihan: Menyediakan log audit yang sempurna dan kemampuan untuk memulihkan sistem ke titik waktu mana pun di masa lalu (point-in-time recovery).
Request-Response vs Event-Driven #
Untuk memahami mengapa kita membutuhkan EDA, mari kita bandingkan dengan pola Request-Response HTTP sinkron yang umum digunakan.
Tabel di bawah ini memperbandingkan karakteristik teknis antara model Request-Response sinkron dengan Event-Driven asinkron secara komprehensif:
| Karakteristik | Request-Response (Synchronous) | Event-Driven (Asynchronous) |
|---|---|---|
| Sifat Komunikasi | Sinkron (Blocking). Pengirim menunggu jawaban secara real-time. | Asinkron (Non-blocking). Pengirim langsung melanjutkan eksekusi. |
| Ketergantungan (Coupling) | Erat (Tight Coupling). Pengirim harus tahu persis alamat penerima. | Longgar (Loose Coupling). Pengirim hanya tahu alamat broker. |
| Resiliensi Sistem | Rendah. Jika penerima mati, transaksi langsung gagal (cascading failure). | Tinggi. If penerima mati, event aman mengantre di broker. |
| Performa (Latency) | Dibatasi oleh kecepatan respons layanan terhambat di rantai API. | Sangat cepat bagi client karena proses berat didelegasikan. |
| Konsistensi Data | Konsistensi Instan (Immediate Consistency). | Konsistensi Tertunda (Eventual Consistency). |
| Skenario Ideal | Operasi baca data cepat (GET /profile) atau validasi instan. |
Pemrosesan background job, ingestion data, pipeline asinkron. |
Tiga Kategori Broker: Message Queue vs Event Streaming vs Event Router #
Jantung dari Event-Driven Architecture adalah Event Broker — infrastruktur perantara yang bertugas menerima event dari producer dan menyalurkannya ke consumer secara aman. Kita harus memilih tipe broker yang tepat sesuai dengan kebutuhan bisnis kita:
1. Message Queue (MQ) #
Message Queue dirancang untuk komunikasi point-to-point atau publish-subscribe tingkat dasar di mana event dibuang segera setelah berhasil diproses oleh consumer.
- Karakteristik: Pesan bersifat sementara (ephemeral), fokus pada transaksi terdistribusi, dan memiliki fitur routing yang kaya.
- Contoh Teknologi: RabbitMQ, ActiveMQ, AWS SQS.
2. Event Streaming #
Event Streaming dirancang untuk menangani jutaan event per detik dengan menyimpan event secara permanen ke dalam disk dalam bentuk log yang terurut (append-only log).
- Karakteristik: Pesan bersifat persisten, mendukung pembacaan ulang event historis (replayability), dan di-scale secara horizontal melalui sistem partisi.
- Contoh Teknologi: Apache Kafka, Apache Pulsar, AWS Kinesis.
3. Event Router #
Event Router dirancang untuk arsitektur serverless guna merutekan event dari berbagai sumber secara dinamis berdasarkan aturan filter deklaratif tanpa mengelola server antrean.
- Karakteristik: Serverless, berbasis skema JSON, terintegrasi erat dengan ekosistem cloud provider.
- Contoh Teknologi: AWS EventBridge, Google Cloud Eventarc.
| Kriteria | Message Queue (RabbitMQ) | Event Streaming (Kafka) | Event Router (EventBridge) |
|---|---|---|---|
| Daya Tahan Pesan | Dihapus setelah dikonsumsi. | Disimpan permanen (durasi waktu/ukuran disk). | Dilewatkan langsung (tanpa penyimpanan). |
| Replay Event | Tidak Bisa. | Bisa (tinggal mengatur ulang offset pembacaan). | Tidak Bisa (kecuali mengaktifkan arsip). |
| Skala Throughput | Sedang (Ribuan pesan/detik). | Sangat Tinggi (Jutaan pesan/detik). | Tinggi (Terbatasi oleh kuota cloud API). |
| Pola Konsumsi | Bersifat Pull/Push ke antrean antarmuka. | Konsumsi asinkron berdasarkan urutan partisi. | Mendorong (Push) event ke target endpoint. |
Kunci Desain EDA: Mengatasi Out-of-Order Execution #
Salah satu tantangan operasional paling membingungkan di dalam arsitektur asinkron terdistribusi adalah Out-of-Order Execution — yaitu kondisi di mana urutan kedatangan event di sisi penerima terbalik dibandingkan urutan pengiriman asli dari sisi pengirim.
- Skenario Masalah: Layanan User mengirimkan event
UserCreateddiikuti olehUserUpdated. Namun karena masalah routing jaringan, eventUserUpdatedtiba di layanan Analytics lebih cepat dibandingkan eventUserCreated. Jika Analytics langsung memproses event update, ia akan memicu error karena entitas user tersebut belum terdaftar. - Solusi Urutan Partisi (Partition Keys): Di dalam Kafka, kita wajib menentukan Partition Key yang konsisten (misalnya userID). Ini menjamin bahwa seluruh event yang berkaitan dengan satu pengguna yang sama akan selalu diarahkan ke partisi log fisik yang sama, sehingga urutan pemrosesan dijamin tetap terurut sekuensial.
- Solusi Versi Entitas (Optimistic Concurrency): Kita menyematkan nomor versi (sequence number/timestamp) di dalam payload event. Penerima akan membandingkan nomor versi event yang masuk dengan versi data yang tersimpan di database lokalnya. Jika event yang masuk memiliki versi yang lebih lama dari data saat ini, event tersebut akan diabaikan secara aman.
Manajemen Evolusi Skema Event (Schema Registry) #
Ketika kita meluncurkan sistem berskala besar dengan banyak tim yang mengelola layanan berbeda, evolusi format skema data event menjadi tantangan yang sangat rawan memicu kerusakan sistem. Producer yang memodifikasi tipe data atau menghapus kolom secara sepihak dapat menyebabkan puluhan consumer mengalami crash parsing secara mendadak.
Untuk mengantisipasi hal ini, arsitektur event modern menggunakan Schema Registry (seperti Confluent Schema Registry). Schema registry bertindak sebagai database terpusat yang menyimpan skema kontrak event (menggunakan format seperti Apache Avro, Protobuf, atau JSON Schema).
Proses validasi skema mengikuti beberapa mode kompatibilitas:
- Backward Compatibility: Consumer versi baru dapat membaca event yang diproduksi oleh producer versi lama. Ini memungkinkan kita memperbarui aplikasi consumer terlebih dahulu tanpa khawatir.
- Forward Compatibility: Consumer versi lama dapat membaca event baru yang dikirim oleh producer versi baru dengan cara mengabaikan kolom baru yang tidak ia ketahui.
- Full Compatibility: Memenuhi kriteria backward dan forward sekaligus, menjamin transisi sistem berjalan 100% mulus tanpa risiko breaking changes.
Tantangan Utama EDA dan Solusi Pola Implementasi #
Meskipun menawarkan skalabilitas yang mengagumkan, membangun aplikasi berbasis event mengharuskan kita menyelesaikan beberapa tantangan kompleks:
1. Jaminan Pengiriman dan Masalah Duplikasi (At-Least-Once Delivery) #
Di dalam jaringan terdistribusi di cloud, sebagian besar broker menjamin pengiriman pesan dengan tingkat At-Least-Once Delivery (Pesan dijamin sampai minimal satu kali, namun ada kemungkinan terkirim lebih dari satu kali akibat kegagalan ACK jaringan).
Karena itu, consumer kita wajib bersifat Idempotent (Idempotent Consumer) — yaitu mampu memproses event yang sama berulang kali tanpa mengubah status data lebih dari satu kali.
Berikut adalah contoh implementasi pola Idempotent Consumer di tingkat kode menggunakan Go dan Redis distributed lock (SETNX) untuk memastikan transaksi pembayaran tidak dieksekusi ganda:
package main
import (
"context"
"fmt"
"time"
"github.com/go-redis/redis/v8"
)
var ctx = context.Background()
type PaymentConsumer struct {
redisClient *redis.Client
}
// ProcessPaymentEvent memproses event pembayaran secara idempotent
func (c *PaymentConsumer) ProcessPaymentEvent(eventID string, orderID string, amount float64) error {
// ✓ BENAR: Gunakan Redis SETNX sebagai distributed lock / token keunikan
// Kunci dibuat unik berdasarkan eventID yang dikirim oleh broker
lockKey := fmt.Sprintf("processed_event:%s", eventID)
// Mencoba mengunci status event selama 24 jam
isNewEvent, err := c.redisClient.SetNX(ctx, lockKey, "processed", 24*time.Hour).Result()
if err != nil {
return fmt.Errorf("gagal mengakses redis cache: %w", err)
}
// ✗ ANTI-PATTERN: Memproses event langsung tanpa mengecek duplikasi pesan
if !isNewEvent {
// Event sudah pernah diproses sebelumnya, abaikan secara anggun
fmt.Printf("Event %s sudah pernah diproses. Mengabaikan event duplikat...\n", eventID)
return nil
}
// Memproses transaksi bisnis inti (Pembayaran)
fmt.Printf("Memproses pembayaran untuk Order %s sebesar %.2f...\n", orderID, amount)
// Simulasi logic pembayaran di database...
return nil
}
2. Transaksi Terdistribusi dan Pola Saga (Saga Pattern) #
Karena layanan-layanan di EDA bersifat terpisah dan tidak menggunakan database yang sama, kita tidak bisa menggunakan transaksi database ACID tradisional (BEGIN TRANSACTION -> COMMIT/ROLLBACK) untuk menjamin konsistensi data lintas layanan.
Kita menggunakan Saga Pattern — serangkaian transaksi lokal mandiri di setiap layanan yang dikoordinasikan secara asinkron. Jika salah satu langkah transaksi lokal gagal di tengah jalan, Saga akan memicu Compensating Transactions (transaksi kompensasi / pembatalan) untuk membalikkan langkah-langkah yang sudah terlanjur sukses sebelumnya guna menjaga integritas data sistem.
Ada dua jenis koordinasi dalam pola Saga:
- Choreography: Setiap layanan mendengarkan event dari layanan lain dan secara otomatis memicu langkah berikutnya tanpa ada koordinator terpusat.
- Orchestration: Sebuah layanan khusus bertindak sebagai orkestrator terpusat yang mengirimkan perintah transaksi dan mengoordinasikan langkah-langkah kompensasi jika terjadi kegagalan.
flowchart TD
CreateOrder["1. Layanan Order: Buat Pesanan (Sukses)"] --> ChargePayment["2. Layanan Bayar: Potong Saldo (Gagal)"]
ChargePayment --> TriggerCompensate["Picu Transaksi Kompensasi"]
TriggerCompensate --> CancelOrder["3. Layanan Order: Batalkan Pesanan (Kompensasi)"]
Ringkasan #
- Event-Driven Architecture (EDA) menggunakan pesan asinkron (Event) sebagai pemicu komunikasi antar-layanan untuk memutus ketergantungan erat.
- Event bersifat immutable karena mencatat fakta sejarah sistem yang telah terjadi di masa lalu dan dinamai menggunakan kata kerja lampau.
- Message Queue cocok untuk transaksi jangka pendek yang ephemeral, sedangkan Event Streaming cocok untuk throughput tinggi yang membutuhkan replay data.
- Tiga pola desain event meliputi Event Notification (thin), Event-Carried State Transfer (fat), dan Event Sourcing (aliran log status).
- Atasi out-of-order execution dengan mendesain partition keys di tingkat broker atau menyematkan nomor versi optimistik pada entitas payload.
- Gunakan Schema Registry untuk mengamankan perubahan kontrak skema event dari kegagalan parsing terdistribusi di produksi.
- Consumer wajib dirancang bersifat Idempotent menggunakan distributed lock (Redis) guna mengantisipasi bahaya duplikasi pesan akibat pengiriman ulang broker.
- Terapkan Saga Pattern dengan transaksi kompensasi asinkron untuk mengelola konsistensi data terdistribusi menggantikan transaksi ACID database tradisional.
← Sebelumnya: Immutable Infrastructure Berikutnya: Twelve-Factor App →