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"]
    end

Perbedaan 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 #

AspekRequest-ResponseEvent-Driven
KomunikasiSinkron — caller menungguAsinkron — producer tidak menunggu
CouplingTight — caller tahu siapa yang dipanggilLoose — producer tidak tahu consumer
Error propagationLangsung — downstream error = upstream errorTerisolasi — satu consumer gagal tidak pengaruhi lain
SkalabilitasProducer dan consumer scale bersamaConsumer bisa scale independen
Cocok untukCRUD, query, operasi yang butuh hasil instanWorkflow, side effects, notifikasi, integrasi
DebuggingMudah — stack trace linearSulit — perlu distributed tracing
ConsistencyStrong consistency lebih mudahMostly 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
    end

Empat 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.

BrokerKekuatan UtamaCocok Untuk
Apache KafkaThroughput sangat tinggi, durable log, replayEvent streaming, audit log, analitik
RabbitMQFlexible routing, mature, mudah setupBackground job, task queue
Google Pub/SubManaged, global, auto-scaleGCP ecosystem, serverless
AWS SNS + SQSNative AWS integration, fan-out patternAWS ecosystem
NATSUltra-low latency, lightweightReal-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]
    end

2. 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
AspekEvent NotificationEvent-Carried State Transfer
Coupling dataRendahTinggi (data duplikat)
Payload sizeKecilBesar
Consumer independensiRendah (perlu query balik)Tinggi
KonsistensiLebih kuat (selalu ambil terbaru)Mungkin stale
Cocok untukData yang sering berubahConsumer 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"]
    end

Anti-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: #

KondisiPenjelasan
Sistem besar dengan banyak integrasiBanyak service perlu bereaksi terhadap kejadian yang sama
Perlu isolasi kegagalanSatu service gagal tidak boleh merembet ke service lain
Side effects yang tidak perlu sinkronEmail, notifikasi, analytics, audit log
Consumer perlu scale independenTraffic ke Email Service berbeda dari Payment Service
Audit trail dan replay dibutuhkanEvent log sebagai sumber kebenaran historis

Jangan Gunakan EDA Jika: #

KondisiAlasan
Aplikasi CRUD sederhanaOverhead tidak sebanding, kompleksitas tidak perlu
Tim belum punya observabilityDebugging jadi mimpi buruk tanpa distributed tracing
Strong consistency wajibEDA mengarah ke eventual consistency by nature
Tim kecil tanpa kapasitas maintain brokerKafka 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, bukan PayOrder.
  • 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 →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact