Event-Driven Architecture #

Ada perbedaan mendasar antara sistem yang tumbuh dengan sehat dan sistem yang makin lama makin sulit diubah: pada sistem yang sehat, menambahkan fitur baru tidak berarti harus memodifikasi komponen yang sudah ada. Event-Driven Architecture (EDA) adalah pendekatan arsitektur yang membuat pertumbuhan itu mungkin — komponen berkomunikasi bukan dengan memanggil satu sama lain secara langsung, melainkan dengan menerbitkan fakta bahwa sesuatu telah terjadi. Siapa yang bereaksi, berapa banyak yang bereaksi, dan apa yang mereka lakukan adalah urusan masing-masing — producer tidak perlu tahu. Panduan ini membahas EDA dari definisi dan perbedaannya dengan request-response, empat komponen utama, tiga pola implementasi, tantangan nyata yang sering diremehkan, hubungannya dengan microservices, hingga checklist kesiapan sebelum mengadopsi pendekatan ini.

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 antar service. Sebuah event adalah fakta yang tidak bisa dibatalkan bahwa sesuatu telah terjadi di dalam sistem — bukan perintah, bukan request, tapi rekaman kejadian.

// Bukan event — ini command/request (imperative)
"Kirim email ke user X"
"Proses pembayaran untuk order Y"

// Ini event — fakta yang sudah terjadi (declarative)
"UserRegistered"
"OrderCreated"
"PaymentCompleted"
"InventoryDepleted"

Perbedaan ini bukan sekadar penamaan — ia mengubah cara sistem berinteraksi secara fundamental. Command mendefinisikan apa yang harus terjadi, sementara event hanya mencatat apa yang sudah terjadi. Producer tidak tahu siapa yang akan bereaksi dan bagaimana caranya.


Perbandingan dengan Request-Response #

Untuk memahami mengapa EDA muncul, perlu dipahami dulu keterbatasan pola request-response yang mendominasinya.

Request-Response (tight coupling):

Order Service ──POST /process──→ Payment Service ──POST /send──→ Email Service
                                        │                               │
                              jika Payment Service down         jika Email Service down
                              Order Service ikut gagal          Payment Service ikut gagal
Event-Driven (loose coupling):

Order Service ──publish──→  [order.created]  ──→  Payment Service
                                             ──→  Email Service
                                             ──→  Analytics Service
                                             ──→  Fraud Detection
                                (semua subscribe independen)

Perbedaan yang paling terasa dalam praktik:

AspekRequest-ResponseEvent-Driven
CouplingTight — pemanggil tahu yang dipanggilLoose — producer tidak tahu consumer
Failure propagationLangsung — A gagal karena B gagalTerisolasi — A tetap jalan walau B down
Penambahan fiturPerlu modifikasi service yang adaTambah consumer baru, producer tidak berubah
Konsistensi dataStrong consistency lebih mudahEventual consistency
DebuggingStack trace linearPerlu distributed tracing
Cocok untukCRUD, query, transaksi atomicWorkflow, side effects, integrasi

Pilihan antara keduanya bukan hitam-putih — sistem yang mature biasanya menggunakan keduanya sesuai konteks. Request-response untuk operasi yang butuh strong consistency dan respons langsung, EDA untuk workflow yang melibatkan banyak komponen independen.


Empat Komponen Utama EDA #

1. Event Producer #

Producer adalah komponen yang menghasilkan event ketika sesuatu terjadi di domain-nya. Tanggung jawab producer hanya satu: mencatat fakta dengan akurat dan mempublikasikannya. Producer tidak peduli siapa yang mendengarkan dan apa yang mereka lakukan.

// BENAR: producer hanya fokus pada domain-nya sendiri
type OrderService struct {
    repo      OrderRepository
    eventBus  EventBus
}

func (s *OrderService) CreateOrder(ctx context.Context, req CreateOrderRequest) (*Order, error) {
    order := &Order{
        ID:        uuid.New().String(),
        UserID:    req.UserID,
        Items:     req.Items,
        Total:     calculateTotal(req.Items),
        Status:    "pending",
        CreatedAt: time.Now(),
    }

    if err := s.repo.Save(ctx, order); err != nil {
        return nil, fmt.Errorf("save order: %w", err)
    }

    // Publish event — tidak tahu siapa yang subscribe, tidak peduli
    event := OrderCreatedEvent{
        EventID:     uuid.New().String(),
        EventType:   "order.created",
        OccurredAt:  time.Now(),
        OrderID:     order.ID,
        UserID:      order.UserID,
        Total:       order.Total,
        ItemCount:   len(order.Items),
    }

    if err := s.eventBus.Publish(ctx, event); err != nil {
        // ✓ Log tapi tidak gagalkan order — bisa pakai outbox pattern untuk guarantee
        log.Errorf("failed to publish order.created for order %s: %v", order.ID, err)
    }

    return order, nil
}

2. Event Consumer #

Consumer adalah komponen yang mendaftarkan diri untuk mendengarkan event tertentu dan menjalankan logikanya. Satu event bisa memiliki banyak consumer independen, dan masing-masing consumer tidak tahu keberadaan consumer lainnya.

// Tiga consumer independen untuk event yang sama

// Consumer A — Payment Service
func (h *PaymentHandler) OnOrderCreated(ctx context.Context, event OrderCreatedEvent) error {
    log.Infof("payment: processing order %s, amount %d", event.OrderID, event.Total)
    return h.paymentProcessor.InitiatePayment(ctx, event.OrderID, event.UserID, event.Total)
}

// Consumer B — Notification Service
func (h *NotificationHandler) OnOrderCreated(ctx context.Context, event OrderCreatedEvent) error {
    return h.notifier.SendOrderConfirmation(ctx, event.UserID, event.OrderID)
}

// Consumer C — Analytics Service (bisa ditambah kapan saja tanpa ubah producer)
func (h *AnalyticsHandler) OnOrderCreated(ctx context.Context, event OrderCreatedEvent) error {
    return h.tracker.Track("order_created", map[string]interface{}{
        "order_id":   event.OrderID,
        "user_id":    event.UserID,
        "total":      event.Total,
        "item_count": event.ItemCount,
    })
}

3. Event Broker — Jantung EDA #

Broker adalah infrastruktur yang menjembatani producer dan consumer. Ia yang memastikan event terkirim, di-buffer saat consumer lambat, dan bisa di-retry saat consumer gagal.

Peran broker dalam EDA:

Producer                    Broker                    Consumer
────────                    ──────                    ────────
publish event  ──────────→  buffering        ──────→  consume & process
                            ordering                  retry jika gagal
                            delivery guarantee        DLQ jika exhausted
                            fan-out ke banyak consumer
                            retention (bisa replay)

Pilihan broker dan karakteristiknya:

Broker          Karakteristik Utama                    Cocok Untuk
──────────────  ──────────────────────────────────     ────────────────────────────
Apache Kafka    High throughput, log-based, replayable  Event streaming, audit trail
RabbitMQ        Flexible routing, AMQP protocol         Complex routing, task queue
AWS SQS/SNS     Managed, serverless-friendly            AWS ecosystem, simplicity
Google Pub/Sub  Managed, global, strong ordering        GCP ecosystem
Azure Event Hub High throughput, Kafka-compatible       Azure ecosystem
NATS            Ultra-low latency, lightweight          Real-time, edge computing

4. Event Schema dan Contract #

Event adalah kontrak antar service. Bukan sekadar JSON yang dikirim — ia adalah interface yang harus dijaga stabilitasnya, karena banyak consumer bergantung padanya.

// BENAR: event schema yang lengkap dengan metadata wajib
{
  "event_id":    "550e8400-e29b-41d4-a716-446655440000",
  "event_type":  "order.created",
  "version":     "2",
  "occurred_at": "2026-04-17T10:30:00Z",
  "source":      "order-service",
  "correlation_id": "req-abc-123",
  "payload": {
    "order_id":   "ORD-2026-001",
    "user_id":    "USR-456",
    "total":      250000,
    "currency":   "IDR",
    "item_count": 3
  }
}

Field event_id, event_type, version, occurred_at, dan correlation_id adalah metadata wajib. event_id untuk idempotency, version untuk evolusi schema, correlation_id untuk distributed tracing.

Perubahan event schema adalah breaking change. Menghapus field, mengubah tipe data, atau mengubah nama field akan merusak semua consumer yang bergantung padanya — dan kamu mungkin tidak tahu ada berapa consumer. Selalu gunakan versioning (order.created.v2) atau backward-compatible changes (hanya menambah field optional baru) saat schema perlu berevolusi.

Tiga Pola Utama dalam EDA #

Pola 1 — Publish-Subscribe #

Ini pola yang paling fundamental: satu producer menerbitkan ke satu topic, banyak consumer subscribe ke topic yang sama. Setiap consumer menerima salinan event yang sama.

Topic: order.created
    │
    ├─── Consumer Group: payment-service    (satu instance per group yang consume)
    ├─── Consumer Group: email-service
    ├─── Consumer Group: analytics-service
    └─── Consumer Group: fraud-detection

Di Kafka, consumer group memungkinkan horizontal scaling di dalam satu service: beberapa instance payment-service membagi beban memproses event dari topic yang sama.

Pola 2 — Event Notification vs Event-Carried State Transfer #

Ini adalah keputusan desain yang sering diabaikan tapi punya konsekuensi besar.

Event Notification hanya membawa sinyal bahwa sesuatu terjadi, bukan datanya. Consumer yang butuh detail harus query sendiri ke service asal.

// Event Notification — minimal payload, consumer query sendiri
type OrderCreatedNotification struct {
    EventID   string    `json:"event_id"`
    OrderID   string    `json:"order_id"`   // hanya ID, bukan data lengkap
    OccurredAt time.Time `json:"occurred_at"`
}

// Consumer harus memanggil Order Service untuk mendapat detail
func (h *PaymentHandler) OnOrderCreated(ctx context.Context, event OrderCreatedNotification) error {
    order, err := h.orderClient.GetOrder(ctx, event.OrderID) // ← tambahan network call
    if err != nil {
        return err
    }
    return h.processPayment(ctx, order)
}

Event-Carried State Transfer membawa semua data yang dibutuhkan consumer di dalam payload event itu sendiri.

// Event-Carried State Transfer — payload lengkap, consumer tidak perlu query
type OrderCreatedEvent struct {
    EventID    string      `json:"event_id"`
    OrderID    string      `json:"order_id"`
    UserID     string      `json:"user_id"`
    UserEmail  string      `json:"user_email"`  // data dari User Service, di-embed
    Total      int64       `json:"total"`
    Currency   string      `json:"currency"`
    Items      []OrderItem `json:"items"`
    OccurredAt time.Time   `json:"occurred_at"`
}

// Consumer langsung pakai data dari event, tidak perlu query tambahan
func (h *PaymentHandler) OnOrderCreated(ctx context.Context, event OrderCreatedEvent) error {
    return h.processPayment(ctx, event.OrderID, event.UserID, event.Total)
}

Trade-off keduanya:

                    Event Notification       Event-Carried State Transfer
─────────────────   ──────────────────────   ──────────────────────────────
Payload size        Kecil                    Besar
Network calls       Lebih banyak (query)     Lebih sedikit
Data freshness      Selalu fresh (query saat itu)  Snapshot saat event terjadi
Coupling data       Rendah                   Lebih tinggi (schema lebih besar)
Consumer autonomy   Perlu akses ke source    Bisa berjalan independen

Tidak ada yang selalu benar — pilih berdasarkan kebutuhan konkret setiap use case.

Pola 3 — Event Sourcing #

Event sourcing adalah pola di mana event log adalah source of truth — state tidak disimpan langsung, melainkan direkonstruksi dari urutan event sejak awal.

Tanpa Event Sourcing (state saat ini):
  orders table: {order_id: 1, status: "shipped", total: 250000}

Dengan Event Sourcing (log event):
  order.created   → {order_id: 1, total: 250000, status: "pending"}
  order.paid      → {order_id: 1, payment_id: "PAY-001"}
  order.shipped   → {order_id: 1, tracking: "JNE-123"}

  State saat ini = hasil replay semua event dari awal

Event sourcing memberikan audit trail lengkap secara gratis dan membuat replay (rebuild state setelah bug fix) menjadi natural. Tapi ia membawa kompleksitas tersendiri — snapshot untuk performa, query yang lebih complex — dan tidak cocok untuk semua domain.


Tantangan Nyata EDA #

Idempotency Consumer #

Karena broker beroperasi dengan at-least-once delivery, consumer akan menerima event yang sama lebih dari sekali saat terjadi retry atau redelivery. Consumer yang tidak idempotent akan menghasilkan data duplikat.

// ANTI-PATTERN: consumer tidak idempotent
func (h *PaymentHandler) OnOrderCreated(event OrderCreatedEvent) error {
    // Jika event ini dikirim ulang, payment akan diproses dua kali
    return h.gateway.Charge(event.UserID, event.Total)
}

// BENAR: consumer idempotent dengan pengecekan event ID
func (h *PaymentHandler) OnOrderCreated(ctx context.Context, event OrderCreatedEvent) error {
    // Cek apakah event ini sudah pernah diproses
    if processed, _ := h.processedEvents.Exists(ctx, event.EventID); processed {
        log.Infof("event %s already processed, skipping", event.EventID)
        return nil
    }

    return h.db.Transaction(func(tx *gorm.DB) error {
        if err := h.gateway.Charge(event.UserID, event.Total); err != nil {
            return err
        }
        // Tandai event sebagai processed dalam transaction yang sama
        return tx.Create(&ProcessedEvent{
            EventID:     event.EventID,
            ProcessedAt: time.Now(),
        }).Error
    })
}

Event Versioning #

Schema event tidak bisa diubah sembarangan karena consumer mungkin berjalan di versi lama. Strategi evolusi yang umum adalah backward-compatible changes dan versioned topics.

// Backward-compatible: hanya tambah field optional — consumer lama tidak rusak
// OrderCreatedEvent v1 → v2: tambah field ShippingAddress (optional)
type OrderCreatedEventV2 struct {
    // Semua field v1 tetap ada
    EventID         string    `json:"event_id"`
    OrderID         string    `json:"order_id"`
    UserID          string    `json:"user_id"`
    Total           int64     `json:"total"`
    OccurredAt      time.Time `json:"occurred_at"`

    // Field baru di v2 — pointer/optional agar consumer v1 tidak rusak
    ShippingAddress *Address  `json:"shipping_address,omitempty"`
    PromoCode       *string   `json:"promo_code,omitempty"`
}

// Breaking change: gunakan topic/event type baru
// order.created.v1 → tetap untuk consumer lama
// order.created.v2 → untuk consumer baru
// Jalankan dual-publish selama masa migrasi, lalu sunset v1

Distributed Tracing #

Tanpa tracing yang benar, debugging di EDA adalah mimpi buruk — error terjadi di consumer yang berjalan di service berbeda, menit setelah request aslinya.

// BENAR: propagasi trace context dari HTTP request ke event ke consumer
func (h *OrderHandler) CreateOrder(w http.ResponseWriter, r *http.Request) {
    // Extract trace context dari incoming request
    ctx, span := tracer.Start(r.Context(), "order.create")
    defer span.End()

    order, _ := h.service.CreateOrder(ctx, parseRequest(r))

    // Inject trace context ke dalam event
    event := OrderCreatedEvent{
        EventID:       uuid.New().String(),
        OrderID:       order.ID,
        TraceID:       span.SpanContext().TraceID().String(),   // ← propagate
        CorrelationID: r.Header.Get("X-Correlation-ID"),        // ← propagate
        OccurredAt:    time.Now(),
    }
    h.eventBus.Publish(ctx, event)
}

// Consumer mengextract trace context untuk melanjutkan trace
func (h *PaymentHandler) OnOrderCreated(ctx context.Context, event OrderCreatedEvent) error {
    ctx, span := tracer.Start(ctx, "payment.process",
        trace.WithRemoteSpanContext(extractSpanContext(event.TraceID)))
    defer span.End()

    // Sekarang trace dari HTTP request → order service → payment service tersambung
    return h.processPayment(ctx, event)
}
EDA tanpa distributed tracing adalah sistem yang tidak bisa dioperasikan di production. Investasikan OpenTelemetry atau solusi tracing lainnya sebelum mengadopsi EDA secara serius, bukan sesudah insiden pertama.

EDA dan Microservices #

EDA bukan syarat untuk microservices, tapi keduanya sangat saling melengkapi. Microservices yang berkomunikasi secara synchronous penuh justru sering berakhir menjadi distributed monolith — semua service saling tergantung, satu down menyebabkan chain failure, dan deployment harus terkoordinasi.

Distributed Monolith (microservices tanpa EDA):

  Service A ──sync──→ Service B ──sync──→ Service C ──sync──→ Service D
                                                                   │
                                                               down
                                                                   │
                            A, B, C semua gagal ←─────────────────┘

EDA yang benar (decoupled):

  Service A ──event──→ [broker] ──→ Service B  (berjalan independen)
                                ──→ Service C  (berjalan independen)
                                ──→ Service D  (down → event terakumulasi di broker)
  Service A tetap berjalan normal
  D akan proses event saat kembali online

EDA memungkinkan setiap service memiliki deployment lifecycle yang benar-benar independen — tidak ada koordinasi deploy yang diperlukan antara producer dan consumer selama schema contract dijaga.


Checklist Kesiapan Adopsi EDA #

Sebelum mengadopsi EDA, pastikan semua ini sudah ada atau sudah direncanakan:

INFRASTRUKTUR:
  □ Message broker sudah dipilih dan dikonfigurasi (Kafka, SQS, RabbitMQ, dll)
  □ DLQ (Dead Letter Queue) tersedia untuk setiap topic kritis
  □ Monitoring broker: queue depth, consumer lag, error rate

ENGINEERING PRACTICE:
  □ Semua consumer didesain idempotent (pengecekan event ID)
  □ Schema event punya field standar: event_id, version, occurred_at, correlation_id
  □ Proses perubahan schema didefinisikan (backward-compatible atau versioned topic)
  □ Outbox pattern dipakai untuk garantee event delivery

OBSERVABILITY:
  □ Distributed tracing tersedia (OpenTelemetry atau equivalent)
  □ Correlation ID dipropagasi dari request ke event ke consumer
  □ Structured logging dengan event_id dan correlation_id di semua service
  □ Alert untuk consumer lag yang menumpuk

OPERASIONAL:
  □ Runbook untuk replay event dari DLQ
  □ Runbook untuk suspend consumer saat ada bug kritis
  □ Cara untuk audit: event mana yang sudah diproses, mana yang belum

Kapan EDA Cocok dan Tidak #

GUNAKAN EDA jika:
  ✓ Sistem punya banyak side effect yang perlu terjadi saat satu aksi terjadi
  ✓ Banyak service independen yang perlu bereaksi pada kejadian yang sama
  ✓ Perlu menambah integrasi baru tanpa mengubah service yang sudah ada
  ✓ Workflow panjang yang melibatkan banyak service
  ✓ Skalabilitas consumer harus independen dari producer
  ✓ Audit trail lengkap dibutuhkan (event sourcing)

JANGAN gunakan EDA jika:
  ✗ Aplikasi CRUD sederhana dengan sedikit integrasi
  ✗ Tim belum punya observability matang (tracing, structured logging)
  ✗ Operasi butuh strong consistency dan respons sinkron
  ✗ Sistem kecil di mana complexity EDA tidak sebanding manfaatnya
  ✗ Tim belum familiar — EDA yang salah lebih buruk dari tidak pakai EDA

Anti-Pattern yang Harus Dihindari #

// ✗ Producer tahu consumer — mengalahkan tujuan EDA
func (s *OrderService) CreateOrder(order Order) error {
    s.repo.Save(order)
    s.paymentService.Process(order) // ← ini bukan EDA, ini masih tight coupling
    s.emailService.Send(order)
    return nil
}
// ✓ Producer hanya publish event, consumer subscribe sendiri

// ✗ Event yang terlalu granular — overhead tanpa manfaat
eventBus.Publish("order.item.quantity.updated")
eventBus.Publish("order.item.price.recalculated")
eventBus.Publish("order.total.updated")
// ✓ Satu event bermakna di level bisnis
eventBus.Publish("order.updated")

// ✗ Event tanpa metadata wajib — tidak bisa idempotency, tidak bisa trace
eventBus.Publish(map[string]interface{}{
    "order_id": order.ID,
    "total":    order.Total,
})
// ✓ Selalu sertakan event_id, event_type, version, occurred_at, correlation_id

// ✗ Mengubah schema event yang sudah ada tanpa versioning
// Menghapus field "user_email" dari OrderCreatedEvent
// → semua consumer yang pakai field ini langsung rusak
// ✓ Tambah field baru sebagai optional, atau buat event type baru dengan versi baru

Ringkasan #

  • Event-Driven Architecture mengubah komunikasi antar komponen dari “panggil langsung” menjadi “terbitkan fakta” — producer tidak tahu siapa consumer, consumer tidak tahu siapa producer.
  • Event adalah fakta yang sudah terjadi, bukan command atau request — OrderCreated bukan CreateOrder.
  • Empat komponen utama: producer (terbitkan event), consumer (bereaksi), broker (jantung distribusi), dan schema contract (interface antar service).
  • Tiga pola utama: publish-subscribe (satu event, banyak consumer), event notification vs event-carried state transfer (trade-off payload vs network call), dan event sourcing (log sebagai source of truth).
  • Idempotency consumer wajib — broker at-least-once delivery berarti event bisa datang lebih dari sekali; cek event_id sebelum proses.
  • Schema event adalah breaking contract — selalu versioning atau backward-compatible changes; jangan ubah/hapus field sembarangan.
  • Distributed tracing adalah keharusan, bukan opsional — propagasi trace_id dan correlation_id dari request ke event ke consumer.
  • EDA melengkapi microservices — tanpa EDA, microservices cenderung menjadi distributed monolith dengan chain failure.
  • Bukan silver bullet — EDA membawa kompleksitas nyata; hanya layak jika tim siap dengan observability, schema governance, dan operational maturity.

← Sebelumnya: Async Processing   Berikutnya: Event Streaming →

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