Event-Driven Architecture #
Dalam beberapa tahun terakhir, Event-Driven Architecture (EDA) menjadi salah satu pendekatan paling populer untuk membangun sistem yang scalable, decoupled, dan resilient — mulai dari e-commerce, fintech, hingga platform streaming. Tapi popularitas ini juga membawa jebakan: banyak tim mengadopsi EDA karena terdengar modern, tanpa memahami trade-off nyata yang ia bawa. EDA bukan tentang mengganti semua REST API dengan Kafka. Ia adalah perubahan cara berpikir tentang bagaimana komponen sistem berkomunikasi — dari “saya minta kamu melakukan X” menjadi “saya beritahu semua yang peduli bahwa X telah terjadi.” Artikel ini membahas apa itu EDA dari prinsipnya, empat komponen utama, tiga pola komunikasi yang harus dipahami, empat tantangan teknis yang sering diremehkan, dan panduan kapan EDA adalah keputusan tepat dan kapan ia over-engineering.
Apa Itu Event-Driven Architecture? #
Event-Driven Architecture adalah pendekatan arsitektur di mana alur komunikasi antar komponen sistem didorong oleh event, bukan oleh pemanggilan langsung (direct call atau request-response).
Event adalah fakta bahwa sesuatu telah terjadi di dalam sistem — bukan perintah, bukan request, tapi rekaman kejadian yang sudah terjadi dan tidak bisa di-undo.
Contoh event yang baik: OrderPaid, UserRegistered, StockDepleted, PaymentFailed. Perhatikan bahwa semua menggunakan kata kerja bentuk lampau — karena event adalah fakta historis, bukan instruksi.
flowchart LR
subgraph RequestResponse["Request-Response — Tight Coupling"]
OS1[Order Service] -->|"1. Proses payment"| PS1[Payment Service]
PS1 -->|"2. Kirim email"| ES1[Email Service]
ES1 -->|"3. Update analytics"| AS1[Analytics Service]
PS1 -->|"4. Update inventory"| IS1[Inventory Service]
Note1["Jika Email Service down\n→ seluruh chain gagal"]
end
subgraph EventDriven["Event-Driven — Loose Coupling"]
OS2[Order Service] -->|"publish: OrderPaid"| BK[("(Event Broker\nKafka / Pub/Sub)")]
BK -->|subscribe| PS2[Payment Service]
BK -->|subscribe| ES2[Email Service]
BK -->|subscribe| AS2[Analytics Service]
BK -->|subscribe| IS2[Inventory Service]
Note2["Jika Email Service down\n→ hanya email yang tertunda\nservice lain tetap jalan"]
endPerbedaan mendasar: dalam request-response, producer tahu siapa yang akan mengerjakan permintaannya. Dalam EDA, producer hanya mengumumkan fakta — siapa yang peduli dan bereaksi adalah urusan mereka sendiri.
Perbandingan Fundamental: Request-Response vs Event-Driven #
| Aspek | Request-Response | Event-Driven |
|---|---|---|
| Komunikasi | Sinkron — caller menunggu | Asinkron — producer tidak menunggu |
| Coupling | Tight — caller tahu siapa yang dipanggil | Loose — producer tidak tahu consumer |
| Error propagation | Langsung — downstream error = upstream error | Terisolasi — satu consumer gagal tidak pengaruhi lain |
| Skalabilitas | Producer dan consumer scale bersama | Consumer bisa scale independen |
| Cocok untuk | CRUD, query, operasi yang butuh hasil instan | Workflow, side effects, notifikasi, integrasi |
| Debugging | Mudah — stack trace linear | Sulit — perlu distributed tracing |
| Consistency | Strong consistency lebih mudah | Mostly eventual consistency |
sequenceDiagram
participant OS as Order Service
participant PS as Payment Service
participant ES as Email Service
rect rgb(255, 220, 220)
Note over OS,ES: Request-Response — Jika Email gagal, semua gagal
OS->>PS: processPayment()
PS->>ES: sendReceipt()
ES--xPS: ERROR — SMTP down!
PS--xOS: ERROR — payment failed?
end
rect rgb(220, 255, 220)
Note over OS,ES: Event-Driven — Email gagal, order tetap OK
OS->>OS: publish(OrderPaid)
OS-->>PS: event diterima, payment diproses ✓
OS-->>ES: event diterima, email gagal → retry nanti
Note over OS: Order Service tidak tahu, tidak terpengaruh
endEmpat Komponen Utama EDA #
1. Event Producer #
Komponen yang menghasilkan event ketika sesuatu terjadi di domain-nya. Producer tidak peduli siapa yang mendengarkan — ia hanya bertanggung jawab memastikan event dipublikasikan dengan benar dan mengandung informasi yang cukup.
// ANTI-PATTERN: producer memanggil consumer langsung — masih coupling
func (s *OrderService) CompleteOrder(orderID string) error {
order := s.db.FindOrder(orderID)
order.Status = "COMPLETED"
s.db.Save(order)
// Tight coupling — harus tahu semua downstream
s.paymentService.FinalizePayment(order) // coupling!
s.emailService.SendConfirmation(order) // coupling!
s.loyaltyService.AddPoints(order) // coupling!
return nil
}
// BENAR: producer publish event, tidak tahu siapa consumer-nya
func (s *OrderService) CompleteOrder(orderID string) error {
order := s.db.FindOrder(orderID)
order.Status = "COMPLETED"
s.db.Save(order)
// Publish fakta — siapa yang peduli adalah urusan mereka
s.eventBus.Publish(OrderCompletedEvent{
EventID: uuid.New().String(),
EventType: "order.completed",
OccurredAt: time.Now(),
OrderID: order.ID,
UserID: order.UserID,
TotalAmount: order.TotalAmount,
Items: order.Items,
})
return nil
}
2. Event Consumer #
Komponen yang berlangganan event tertentu dan menjalankan logic bisnis sebagai reaksi. Bisa ada banyak consumer untuk satu event yang sama, dan setiap consumer berjalan secara independen.
// BENAR: setiap consumer independen, failure-isolated
type PaymentConsumer struct{ service PaymentService }
func (c *PaymentConsumer) Handle(event OrderCompletedEvent) error {
return c.service.FinalizePayment(event.OrderID, event.TotalAmount)
}
type EmailConsumer struct{ emailSvc EmailService }
func (c *EmailConsumer) Handle(event OrderCompletedEvent) error {
return c.emailSvc.SendOrderConfirmation(event.UserID, event.OrderID)
}
type LoyaltyConsumer struct{ loyaltySvc LoyaltyService }
func (c *LoyaltyConsumer) Handle(event OrderCompletedEvent) error {
return c.loyaltySvc.CreditPoints(event.UserID, event.TotalAmount)
}
// Setiap consumer punya retry policy sendiri
// Email gagal tidak pengaruhi loyalty points
// Loyalty gagal tidak pengaruhi payment
3. Event Broker / Message Broker #
Perantara yang mengelola delivery event dari producer ke consumer. Broker adalah jantung EDA — ia yang menjamin event tidak hilang, bisa di-buffer, bisa di-retry, dan bisa disampaikan ke semua subscriber.
| Broker | Kekuatan Utama | Cocok Untuk |
|---|---|---|
| Apache Kafka | Throughput sangat tinggi, durable log, replay | Event streaming, audit log, analitik |
| RabbitMQ | Flexible routing, mature, mudah setup | Background job, task queue |
| Google Pub/Sub | Managed, global, auto-scale | GCP ecosystem, serverless |
| AWS SNS + SQS | Native AWS integration, fan-out pattern | AWS ecosystem |
| NATS | Ultra-low latency, lightweight | Real-time, IoT, edge |
4. Event Schema & Contract #
Event bukan sekadar JSON — ia adalah kontrak antar service yang harus dikelola dengan disiplin. Perubahan event schema adalah breaking change yang bisa mematikan consumer yang tidak diupdate.
// Struktur event yang baik — lengkap dengan metadata
type OrderCompletedEvent struct {
// Metadata standar — semua event harus punya ini
EventID string `json:"event_id"` // UUID unik per event
EventType string `json:"event_type"` // "order.completed"
Version int `json:"version"` // versi schema
OccurredAt time.Time `json:"occurred_at"` // kapan event terjadi
ProducerID string `json:"producer_id"` // service mana yang publish
// Payload domain
OrderID string `json:"order_id"`
UserID string `json:"user_id"`
TotalAmount float64 `json:"total_amount"`
Items []OrderItem `json:"items"`
}
Jangan pernah mengubah field yang sudah ada di event schema tanpa versioning. Consumer yang menggunakan field lama akan broken tanpa peringatan. Gunakan order.completed.v2 sebagai event type baru, jalankan keduanya secara paralel selama masa transisi, baru deprecate yang lama.Empat Alasan EDA Dibutuhkan #
1. Mengatasi Tight Coupling #
Dalam sistem yang tumbuh, setiap fitur baru sering berarti menambah pemanggilan baru ke service lain. Seiring waktu, ini menciptakan web dependency yang membuat setiap perubahan berisiko.
flowchart TD
subgraph TightCoupled["❌ Tight Coupling — Web of Dependencies"]
A[Order Service] --> B[Payment Service]
A --> C[Email Service]
A --> D[Inventory Service]
B --> C
B --> E[Fraud Detection]
C --> F[Template Service]
D --> G[Supplier Service]
end
subgraph EDA["✅ EDA — Setiap Service Independen"]
OS[Order Service] --> BK["(Broker)"]
BK --> PS[Payment Service]
BK --> ES[Email Service]
BK --> INV[Inventory Service]
BK --> FR[Fraud Detection]
end2. Skalabilitas Horizontal #
Consumer bisa di-scale independen sesuai beban. Jika Email Service kewalahan saat flash sale, kamu bisa menambah instance Email Consumer tanpa menyentuh Order Service atau broker.
3. Resilience & Fault Isolation #
Kegagalan satu consumer tidak merembet ke producer atau consumer lain. Email Service down saat midnight maintenance tidak membuat order gagal — event tetap tersimpan di broker dan akan diproses saat service pulih.
4. Mendukung Audit Trail Alami #
Karena setiap event adalah fakta historis yang tersimpan, EDA secara alami menghasilkan audit trail lengkap tentang semua yang terjadi di sistem — sangat berharga untuk debugging, compliance, dan analitik.
Tiga Pola Komunikasi dalam EDA #
Pola 1: Publish-Subscribe (Pub/Sub) #
Pola paling umum. Satu event dipublikasikan ke topic, semua subscriber yang terdaftar menerima event tersebut secara independen.
flowchart LR
P["Producer\nOrder Service"] -->|"publish\nOrderPaid"| T[("(Topic:\norder.paid)")]
T -->|"deliver"| C1["Consumer A\nEmail Service"]
T -->|"deliver"| C2["Consumer B\nLoyalty Service"]
T -->|"deliver"| C3["Consumer C\nAnalytics Service"]
T -->|"deliver"| C4["Consumer D\nAudit Logger"]Setiap consumer memiliki offset atau queue sendiri — jika Consumer B crash dan restart, ia akan melanjutkan dari posisi terakhirnya tanpa mempengaruhi Consumer A, C, atau D.
Pola 2: Event Notification vs Event-Carried State Transfer #
Dua pendekatan berbeda untuk isi payload event, masing-masing dengan trade-off yang signifikan:
// Event Notification — hanya sinyal, consumer query sendiri
type OrderPaidEvent_Notification struct {
EventID string `json:"event_id"`
OrderID string `json:"order_id"` // hanya ID, bukan data lengkap
PaidAt string `json:"paid_at"`
}
// Consumer harus query ke Order Service untuk detail lengkap
// + Coupling data rendah, payload kecil
// - Consumer perlu HTTP call tambahan ke producer
// Event-Carried State Transfer — data lengkap di dalam event
type OrderPaidEvent_FullState struct {
EventID string `json:"event_id"`
OrderID string `json:"order_id"`
UserID string `json:"user_id"`
UserEmail string `json:"user_email"`
TotalAmount float64 `json:"total_amount"`
Items []OrderItem `json:"items"`
ShipAddress string `json:"shipping_address"`
PaidAt string `json:"paid_at"`
}
// Consumer punya semua yang dibutuhkan tanpa query tambahan
// + Consumer fully independent, tidak perlu panggil producer
// - Payload besar, data duplikat antar service
| Aspek | Event Notification | Event-Carried State Transfer |
|---|---|---|
| Coupling data | Rendah | Tinggi (data duplikat) |
| Payload size | Kecil | Besar |
| Consumer independensi | Rendah (perlu query balik) | Tinggi |
| Konsistensi | Lebih kuat (selalu ambil terbaru) | Mungkin stale |
| Cocok untuk | Data yang sering berubah | Consumer yang perlu fully autonomous |
Pola 3: Eventual Consistency #
EDA hampir selalu berakhir dengan eventual consistency — data tidak langsung konsisten di semua service, tapi akan konsisten “pada akhirnya”. Engineer harus menerima ini dan mendesain sistem dengan sadar.
// ANTI-PATTERN: mengasumsikan data langsung konsisten setelah publish
func (s *OrderService) GetOrderStatus(orderID string) OrderStatus {
order := s.db.FindOrder(orderID)
s.eventBus.Publish(OrderCreatedEvent{OrderID: orderID})
// SALAH: payment belum tentu langsung diproses!
payment := s.paymentService.GetPayment(orderID) // mungkin belum ada
return buildStatus(order, payment)
}
// BENAR: desain untuk eventual consistency — tampilkan state yang jujur
func (s *OrderService) GetOrderStatus(orderID string) OrderStatus {
order := s.db.FindOrder(orderID)
return OrderStatus{
OrderID: order.ID,
Status: order.Status, // status dari domain sendiri
PaymentStatus: order.PaymentStatus, // di-update oleh event consumer
LastUpdatedAt: order.UpdatedAt,
// Tidak query ke service lain — baca local state yang diupdate via event
}
}
Empat Tantangan Teknis yang Sering Diremehkan #
1. Debugging Lebih Sulit #
Alur eksekusi tidak linear — sebuah aksi bisa memicu chain event yang tersebar di puluhan service. Tanpa distributed tracing, debugging menjadi investigasi buta.
// BENAR: propagasi correlation ID ke setiap event dan log
type BaseEvent struct {
EventID string `json:"event_id"`
CorrelationID string `json:"correlation_id"` // trace dari request awal
OccurredAt time.Time `json:"occurred_at"`
}
func publishWithCorrelation(ctx context.Context, event interface{}) {
correlationID := ctx.Value("correlation_id").(string)
// Set correlation ID sebelum publish
// Semua consumer akan meneruskan ID ini ke event berikutnya
}
// Di consumer: log dengan correlation ID agar bisa di-trace
func (c *EmailConsumer) Handle(ctx context.Context, event OrderPaidEvent) error {
log.WithField("correlation_id", event.CorrelationID).
WithField("event_id", event.EventID).
Info("processing order.paid event")
// ...
}
2. Idempotency Wajib di Semua Consumer #
Message broker menjamin at-least-once delivery — event yang sama bisa diterima lebih dari sekali. Setiap consumer wajib idempotent.
// ANTI-PATTERN: consumer tidak idempotent — email dikirim 2x jika retry
func (c *EmailConsumer) Handle(event OrderPaidEvent) error {
return c.emailSvc.SendReceipt(event.UserID, event.OrderID)
// Jika event di-redeliver → email duplikat!
}
// BENAR: cek event_id sebelum proses — skip jika sudah pernah diproses
func (c *EmailConsumer) Handle(ctx context.Context, event OrderPaidEvent) error {
exists, _ := c.processedEvents.Exists(ctx, event.EventID)
if exists {
log.Infof("event %s already processed, skipping", event.EventID)
return nil
}
if err := c.emailSvc.SendReceipt(event.UserID, event.OrderID); err != nil {
return err
}
c.processedEvents.Mark(ctx, event.EventID)
return nil
}
3. Event Versioning #
Event schema akan berubah seiring berkembangnya bisnis. Perubahan tanpa strategi versioning bisa mematikan consumer tanpa peringatan.
// ANTI-PATTERN: rename field langsung — breaking change!
// Sebelum: {"user_id": "123"}
// Setelah: {"customer_id": "123"} ← consumer lama crash!
// BENAR: backward-compatible dengan field baru ditambah, lama tetap ada
type OrderPaidEvent struct {
EventID string `json:"event_id"`
Version int `json:"version"`
UserID string `json:"user_id"` // tetap ada untuk backward compat
CustomerID string `json:"customer_id"` // field baru di v2
OrderID string `json:"order_id"`
Amount float64 `json:"amount"`
}
// Atau gunakan versioned event type
// v1: "order.paid" → consumer lama tetap berjalan
// v2: "order.paid.v2" → consumer baru subscribe ini
// Jalankan keduanya selama masa transisi, lalu deprecate v1
4. Event Ordering #
Dalam sistem terdistribusi, urutan event tidak terjamin kecuali ada mekanisme khusus. Event OrderCancelled bisa tiba sebelum OrderCreated jika tidak didesain dengan benar.
// BENAR: gunakan partition key agar event untuk entity yang sama
// selalu masuk ke partition yang sama dan diproses berurutan
func publishOrderEvent(event OrderEvent) {
broker.PublishWithKey(
"order-events",
event.OrderID, // partition key = order ID
event,
)
}
// Consumer yang perlu ordering: gunakan single partition consumer
// Consumer yang tidak peduli ordering: bisa pakai multiple partition
EDA dan Microservices: Saling Melengkapi #
EDA bukan syarat untuk microservices, tapi microservices tanpa EDA sangat sering berakhir menjadi distributed monolith — service yang terpisah secara deployment tapi masih tightly coupled secara komunikasi.
flowchart TD
subgraph BadMicro["❌ Distributed Monolith"]
M1[Service A] -->|sync HTTP| M2[Service B]
M2 -->|sync HTTP| M3[Service C]
M3 -->|sync HTTP| M4[Service D]
M4 -->|sync HTTP| M5[Service E]
Note["Deploy terpisah tapi\ncoupling seperti monolith"]
end
subgraph GoodMicro["✅ True Microservices dengan EDA"]
S1[Service A] -->|event| BK["(Broker)"]
BK --> S2[Service B]
BK --> S3[Service C]
BK --> S4[Service D]
BK --> S5[Service E]
Note2["Benar-benar independent\nbisa deploy, scale, fail sendiri"]
endAnti-Pattern yang Harus Dihindari #
// ✗ Event sebagai command — ini bukan event, ini perintah terselubung
type SendEmailCommand struct { // nama "command" sudah salah
To string
Subject string
Body string
}
// ✓ Event adalah fakta yang sudah terjadi
type UserRegisteredEvent struct {
UserID string
Email string
RegisteredAt time.Time
}
// ✗ Publish event dan langsung asumsikan consumer sudah jalan
eventBus.Publish(OrderPaidEvent{...})
payment := paymentService.GetPayment(orderID) // mungkin belum ada — eventual!
// ✗ Tidak ada event schema registry — schema berubah tanpa koordinasi
// ✓ Gunakan schema registry (Confluent Schema Registry, Protobuf, dll)
// ✗ Consumer tidak idempotent
func handle(event Event) { db.Insert(event.Data) } // duplikat jika retry!
// ✓ Cek event_id dulu
// ✗ Tidak ada DLQ — event gagal terus menerus dibuang
// ✓ Semua consumer harus punya DLQ untuk event yang gagal melewati max retry
// ✗ Event terlalu granular — satu CRUD action = satu event
eventBus.Publish(UserFirstNameUpdatedEvent{...})
eventBus.Publish(UserLastNameUpdatedEvent{...})
// ✓ Event pada level bisnis yang meaningful
eventBus.Publish(UserProfileUpdatedEvent{
UserID: id,
Changes: map[string]interface{}{"first_name": "...", "last_name": "..."},
})
Kapan EDA Tepat dan Tidak Tepat #
Gunakan EDA Jika: #
| Kondisi | Penjelasan |
|---|---|
| Sistem besar dengan banyak integrasi | Banyak service perlu bereaksi terhadap kejadian yang sama |
| Perlu isolasi kegagalan | Satu service gagal tidak boleh merembet ke service lain |
| Side effects yang tidak perlu sinkron | Email, notifikasi, analytics, audit log |
| Consumer perlu scale independen | Traffic ke Email Service berbeda dari Payment Service |
| Audit trail dan replay dibutuhkan | Event log sebagai sumber kebenaran historis |
Jangan Gunakan EDA Jika: #
| Kondisi | Alasan |
|---|---|
| Aplikasi CRUD sederhana | Overhead tidak sebanding, kompleksitas tidak perlu |
| Tim belum punya observability | Debugging jadi mimpi buruk tanpa distributed tracing |
| Strong consistency wajib | EDA mengarah ke eventual consistency by nature |
| Tim kecil tanpa kapasitas maintain broker | Kafka butuh expertise untuk dioperasikan dengan benar |
Checklist Implementasi EDA #
DESAIN EVENT:
□ Event menggunakan kata kerja lampau (OrderPaid, bukan PayOrder)
□ Event punya metadata standar: event_id, event_type, version, occurred_at
□ Event schema terdokumentasi dan di-review sebelum publish
□ Versioning strategy sudah ditentukan sebelum ada breaking change
PRODUCER:
□ Producer publish event setelah state berhasil disimpan ke DB
□ Producer tidak tahu dan tidak memanggil consumer langsung
□ Event mengandung data yang cukup untuk consumer bertindak
CONSUMER:
□ Semua consumer idempotent — cek event_id sebelum proses
□ Consumer punya DLQ untuk event yang gagal max retry
□ Consumer tidak memanggil producer balik (avoid circular dependency)
□ Correlation ID dipropagasi ke setiap log dan event downstream
BROKER:
□ Retention period event sudah dikonfigurasi
□ Dead Letter Topic / Queue aktif
□ Monitoring: consumer lag, throughput, error rate
OBSERVABILITY:
□ Distributed tracing aktif (OpenTelemetry / Jaeger)
□ Setiap consumer log event_id dan correlation_id
□ Alert untuk consumer lag yang terlalu tinggi
Ringkasan #
- Event adalah fakta historis — bukan perintah, bukan request; gunakan kata kerja lampau:
OrderPaid, bukanPayOrder.- Producer tidak tahu consumer — ini yang menciptakan loose coupling; producer hanya bertanggung jawab mengumumkan fakta, siapa yang bereaksi adalah urusan mereka.
- Empat komponen utama: producer (menghasilkan event), consumer (bereaksi terhadap event), broker (menjamin delivery), dan event schema (kontrak antar service).
- Pub/Sub adalah pola inti — satu event bisa dikonsumsi banyak consumer secara independen; kegagalan satu consumer tidak mempengaruhi yang lain.
- Event Notification vs Event-Carried State: pilih berdasarkan kebutuhan independensi consumer vs ukuran payload dan risiko stale data.
- Eventual consistency adalah konsekuensi, bukan bug — desain UX dan business rule dengan sadar bahwa data tidak langsung konsisten di semua service.
- Idempotency wajib di semua consumer — at-least-once delivery adalah jaminan broker; event yang sama bisa datang lebih dari sekali.
- Event versioning harus direncanakan sejak awal — perubahan schema tanpa versioning adalah breaking change yang bisa mematikan consumer.
- Distributed tracing dan correlation ID bukan opsional — debugging EDA tanpa tracing seperti menginvestigasi kejadian tanpa saksi.
- EDA bukan silver bullet — CRUD sederhana, tim kecil tanpa observability, dan kebutuhan strong consistency adalah alasan valid untuk tidak menggunakan EDA.
← Sebelumnya: Async Processing Berikutnya: Event Streaming →