Dalam integrasi backend, masalah terbesar sering bukan pada format JSON, melainkan pada semantik: apa arti event itu bagi pengirim dan penerima. Dua sistem bisa sama-sama membaca payload yang valid, tetapi menafsirkan maknanya berbeda. Akibatnya, retry menjadi berbahaya, deduplikasi gagal, status bisnis melompat, atau data tampak konsisten padahal maknanya sudah melenceng.
Itulah inti dari kontrak API untuk event semantik: bukan hanya menyepakati nama field, tetapi juga menyepakati arti, versi, perilaku retry, idempotensi, urutan event, dan cara mendeteksi penyimpangan. Analogi singkatnya mirip upaya “mendekode bahasa”: bukan cukup mendengar bunyi, kita harus memahami konteks dan maksud pesannya. Dalam sistem backend, konteks itu harus ditulis jelas dalam kontrak.
Mengapa event semantik sering gagal di integrasi nyata
Failure mode yang paling sering terjadi adalah ketika satu pihak menganggap event sebagai state notification, sedangkan pihak lain menganggapnya sebagai command atau delta perubahan. Misalnya, event order.updated dikirim setiap ada perubahan pada order. Sistem A menganggap payload terakhir adalah representasi penuh order. Sistem B menganggap payload hanya berisi field yang berubah. Hasilnya, field yang tidak ikut terkirim bisa dianggap null, dihapus, atau diabaikan secara tidak konsisten.
Contoh lain: enum status awalnya hanya PENDING, PAID, dan CANCELLED. Beberapa bulan kemudian, pengirim menambah AUTHORIZED. Jika consumer menulis parser dengan asumsi enum tertutup dan tidak menyiapkan fallback, event bisa ditolak, masuk retry loop, lalu menumpuk di antrean. Secara teknis schema tetap “mirip”, tetapi secara semantik integrasi sudah rusak.
Catatan: Valid JSON tidak berarti kontrak aman. Sebuah payload bisa lolos parsing tetapi tetap salah dipahami secara bisnis.
Prinsip desain kontrak API untuk event semantik
1. Bedakan struktur dari makna
Schema menjelaskan bentuk data; kontrak semantik menjelaskan arti data. Dokumentasi event perlu menyatakan secara eksplisit:
- apa yang dipresentasikan event tersebut,
- kapan event dipancarkan,
- apakah payload adalah snapshot penuh atau partial update,
- apakah event boleh datang lebih dari sekali,
- apakah event harus diproses berurutan,
- bagaimana consumer harus menangani nilai yang belum dikenal.
2. Pilih nama event yang menyampaikan niat
Nama event seperti order.updated terlalu umum bila makna bisnisnya penting. Jika yang terjadi sebenarnya adalah transisi tertentu, nama seperti order.payment_authorized atau order.fulfillment_requested lebih aman. Nama yang spesifik mengurangi ruang interpretasi di sisi consumer.
3. Gunakan envelope metadata yang stabil
Jangan campur metadata transport dengan data bisnis. Sebuah event sebaiknya memiliki envelope yang konsisten, misalnya:
{
"event_id": "evt_01J9Y8...",
"event_type": "order.payment_authorized",
"event_version": "2.1",
"occurred_at": "2026-08-11T10:15:30Z",
"producer": "billing-service",
"aggregate_id": "ord_12345",
"sequence": 18,
"idempotency_key": "bill-auth-ord_12345-18",
"data": {
"order_id": "ord_12345",
"payment_id": "pay_987",
"currency": "IDR",
"amount": 150000
}
}Metadata seperti event_id, event_type, event_version, occurred_at, aggregate_id, dan sequence membantu routing, dedup, observability, dan penanganan ordering tanpa mengotori domain payload.
Schema evolution: field opsional, field wajib, dan enum yang berubah
Tambahkan field baru secara kompatibel
Menambah field baru biasanya aman bila consumer diharapkan mengabaikan field yang tidak dikenal. Karena itu, consumer sebaiknya tidak gagal hanya karena ada field tambahan. Sebaliknya, producer tidak boleh berasumsi semua consumer langsung memahami field baru.
Contoh payload awal:
{
"event_type": "shipment.created",
"event_version": "1.0",
"data": {
"shipment_id": "shp_1001",
"order_id": "ord_123",
"carrier": "internal",
"eta_date": "2026-08-13"
}
}Lalu kebutuhan bisnis berkembang: kini ada jenis carrier eksternal dan nomor pelacakan.
{
"event_type": "shipment.created",
"event_version": "1.1",
"data": {
"shipment_id": "shp_1001",
"order_id": "ord_123",
"carrier": "third_party",
"tracking_number": "TRK-8899123",
"eta_date": "2026-08-13"
}
}Perubahan ini kompatibel jika:
tracking_numberbersifat opsional untuk versi sebelumnya,- consumer tidak mengunci enum
carrierhanya ke satu nilai lama, - dokumentasi menjelaskan kapan
tracking_numberada dan kapan tidak.
Jangan sembrono mengubah field wajib
Perubahan yang paling berbahaya biasanya bukan penambahan field, tetapi perubahan makna field wajib. Misalnya, field amount semula berarti total bruto, lalu diam-diam diubah menjadi neto setelah diskon. Nama field tetap sama, parsing tetap sukses, tetapi seluruh perhitungan consumer menjadi salah.
Bila makna berubah, pilih salah satu:
- buat field baru dengan nama yang lebih jelas, misalnya
gross_amountdannet_amount, - naikkan versi event secara eksplisit,
- pertahankan field lama selama masa transisi.
Opsional vs wajib: aturan praktis
- Wajib bila field dibutuhkan untuk memahami event secara benar.
- Opsional bila field hanya memperkaya konteks atau belum tersedia pada semua skenario.
- Field opsional tetap harus punya semantik jelas: tidak ada,
null, dan string kosong bisa berarti hal yang berbeda.
Kesalahan umum adalah mengubah field wajib menjadi opsional tanpa mendefinisikan perilaku fallback. Consumer lalu membuat asumsi sendiri, dan semantik menyimpang antar tim.
Enum harus diperlakukan sebagai himpunan terbuka
Jangan anggap enum akan selamanya tetap. Consumer yang kuat harus memiliki cabang unknown atau fallback aman. Misalnya:
switch (status) {
case "PENDING":
case "PAID":
case "CANCELLED":
handleKnownStatus(status);
break;
default:
logUnknownEnum(status);
parkForReview();
}Fallback ini penting. Lebih baik menaruh event ke jalur peninjauan atau memproses sebagian dengan aman daripada merusak state karena memaksa enum lama pada makna baru.
Versioning yang masuk akal: kapan perlu dan bagaimana memakainya
Gunakan versi untuk perubahan semantik, bukan setiap perubahan kecil
Jika hanya menambah field opsional yang bisa diabaikan, sering kali tidak perlu membuat versi mayor baru. Tetapi bila ada perubahan berikut, versioning hampir selalu dibutuhkan:
- arti field berubah,
- field wajib ditambah atau dihapus,
- payload berubah dari snapshot penuh menjadi partial update,
- aturan bisnis consumer harus diubah agar tetap benar.
Versi di metadata, bukan hanya di dokumentasi
Simpan versi langsung di payload atau header yang dapat diakses consumer. Jangan bergantung pada “tim sudah tahu” atau tanggal cutover manual. Consumer perlu keputusan deterministik saat memproses event.
Contoh pendekatan yang umum:
- Versi pada event type:
order.created.v2 - Versi pada field metadata:
event_version: "2.0"
Keduanya bisa dipakai. Yang penting konsisten dan mudah dirouting. Banyak tim memilih event_type tetap stabil dan menaruh versi di metadata agar evolusi lebih rapi.
Strategi kompatibilitas mundur
- Tambahkan field baru sebagai opsional.
- Biarkan producer mengirim field lama dan baru selama masa transisi.
- Perbarui consumer agar dapat membaca dua bentuk payload.
- Pasang metrik adopsi versi consumer.
- Baru hapus field lama setelah semua consumer bermigrasi.
Ini lebih lambat, tetapi jauh lebih aman dibanding perubahan serentak yang bergantung pada koordinasi manual lintas tim.
Idempotensi, dedup webhook, dan retry yang aman
Mengapa retry hampir pasti terjadi
Dalam sistem terdistribusi, timeout, putus koneksi, restart worker, dan error 5xx akan terjadi. Karena itu, producer atau delivery platform hampir pasti mengirim event yang sama lebih dari sekali. Jika consumer menganggap setiap request adalah event baru, Anda akan mendapat duplikasi side effect: invoice ganda, email berulang, stok berkurang dua kali, atau status mundur.
Idempotency key bukan sekadar request ID
Idempotency key adalah identitas operasi logis, bukan hanya identitas paket jaringan. Jika event yang sama dikirim ulang lima kali, semua percobaan harus mengacu pada kunci yang sama agar side effect dieksekusi maksimal satu kali.
Contoh tabel dedup sederhana:
CREATE TABLE processed_events (
idempotency_key VARCHAR(128) PRIMARY KEY,
event_id VARCHAR(64) NOT NULL,
event_type VARCHAR(128) NOT NULL,
processed_at TIMESTAMP NOT NULL,
response_code INT NULL
);Alur consumer yang aman:
- Terima webhook atau event dari queue.
- Ambil
idempotency_keyatau turunkan dari kombinasi yang stabil, misalnyaevent_type + aggregate_id + sequence. - Coba simpan ke tabel dedup dalam transaksi.
- Jika insert gagal karena kunci sudah ada, anggap event sudah diproses dan kembalikan sukses idempoten.
- Jika berhasil, jalankan side effect bisnis lalu commit.
Poin pentingnya: cek dedup dan perubahan state bisnis idealnya terjadi dalam unit transaksi yang konsisten. Jika dedup disimpan setelah side effect, crash di tengah proses masih bisa menyebabkan duplikasi saat retry.
Webhook dedup: gunakan event ID, tetapi pahami batasnya
Banyak provider mengirim event_id unik. Ini baik untuk dedup, tetapi tidak selalu cukup untuk idempotensi bisnis. Misalnya, provider bisa mengirim dua event berbeda yang mewakili operasi bisnis yang sama setelah failover. Karena itu:
- gunakan
event_iduntuk dedup transport, - gunakan
idempotency_keyuntuk dedup operasi bisnis, - pastikan TTL penyimpanan dedup cukup lama untuk menutup jendela retry realistis.
Retry aman: bedakan error sementara dan permanen
Tidak semua kegagalan layak di-retry.
- Retry untuk timeout, gangguan jaringan, lock contention sementara, atau dependency yang sementara gagal.
- Jangan retry tanpa batas untuk payload invalid, schema tidak dikenali, signature webhook salah, atau field wajib hilang.
Gunakan backoff bertahap dan dead-letter queue untuk event yang gagal berulang. Tanpa itu, satu payload buruk bisa menghambat antrean panjang dan menutupi akar masalah.
Ordering event: kapan wajib, kapan cukup eventual consistency
Jangan mengasumsikan urutan global
Di sistem event-driven, urutan global hampir selalu mahal atau tidak realistis. Yang biasanya lebih masuk akal adalah menjaga urutan per entitas, misalnya per order_id atau account_id. Karena itu, tambahkan aggregate_id dan bila perlu sequence atau version pada event.
Contoh failure mode akibat ordering
Misalnya consumer menerima dua event:
order.cancelleddengansequence=11order.paiddengansequence=10
Jika order.cancelled datang lebih dulu lalu consumer tidak mengecek sequence, status akhir bisa menjadi PAID saat event kedua tiba belakangan. Padahal secara bisnis order sudah dibatalkan.
Pendekatan yang lebih aman:
- simpan sequence terakhir per aggregate,
- tolak atau parkir event yang lebih lama dari state terkini,
- jika gap sequence terdeteksi, putuskan apakah perlu menunggu, melakukan re-fetch snapshot, atau menerima eventual consistency.
Snapshot vs delta event
Jika ordering sulit dijamin, event snapshot sering lebih mudah dikelola dibanding delta event. Snapshot membawa state terbaru yang cukup lengkap untuk sinkronisasi. Delta lebih hemat payload, tetapi lebih rapuh terhadap event hilang atau datang terlambat.
Pilih snapshot bila:
- consumer banyak dan heterogen,
- sinkronisasi lebih penting daripada efisiensi payload,
- ordering sulit dijaga.
Pilih delta bila:
- throughput tinggi dan payload besar,
- consumer memahami urutan dengan baik,
- ada mekanisme replay atau rekonsiliasi yang matang.
Observability: cara melihat kontrak rusak sebelum jadi insiden besar
Metrik yang sebaiknya ada
- jumlah event per
event_typedan versi, - rate retry dan dead-letter,
- jumlah event duplikat yang tertahan oleh dedup,
- jumlah enum/field tidak dikenal,
- selisih sequence atau out-of-order rate,
- latensi dari
occurred_atke waktu proses consumer.
Logging yang berguna untuk debugging
Minimal log berikut perlu tersedia:
event_id,event_type,event_version,aggregate_iddansequence,- hasil validasi schema,
- keputusan idempotensi: baru, duplikat, atau replay,
- error class: transient atau permanent.
Tanpa metadata ini, tim biasanya hanya melihat “consumer gagal”, tetapi tidak tahu apakah masalahnya versi payload, urutan event, atau duplikasi retry.
Tracing lintas layanan
Jika memungkinkan, teruskan correlation ID atau trace ID dari producer ke consumer. Ini membantu saat satu event memicu beberapa side effect ke sistem lain. Debugging jadi jauh lebih cepat karena satu alur bisnis dapat ditelusuri melintasi service, queue, dan webhook.
Webhook vs polling: kapan memakai yang mana
Pilih webhook bila Anda butuh reaksi cepat
Webhook cocok bila perubahan harus diketahui segera, misalnya status pembayaran, pengiriman, atau provisioning. Kelebihannya adalah latensi rendah dan tidak perlu terus-menerus menanyakan status. Kekurangannya: Anda harus siap menerima retry, duplikasi, urutan yang tidak terjamin, dan kebutuhan verifikasi keamanan seperti signature.
Pilih polling bila sumber event tidak stabil atau kontraknya lemah
Polling cocok bila:
- provider tidak menyediakan webhook yang andal,
- Anda lebih butuh snapshot state terbaru daripada tiap perubahan kecil,
- volume perubahan rendah,
- keterlambatan beberapa detik atau menit masih dapat diterima.
Polling lebih sederhana secara operasional, tetapi lebih boros request dan memiliki latensi lebih tinggi. Dalam beberapa integrasi, pendekatan hibrida justru paling aman: webhook untuk sinyal cepat, polling atau rekonsiliasi periodik untuk memastikan tidak ada event yang terlewat.
Contoh perubahan kontrak: sebelum dan sesudah
Versi awal yang ambigu
{
"event_type": "order.updated",
"data": {
"order_id": "ord_5001",
"status": "PAID",
"amount": 250000
}
}Masalah pada kontrak ini:
order.updatedterlalu umum,- tidak jelas apakah payload snapshot atau delta,
amounttidak jelas bruto atau neto,- tidak ada versi, event ID, sequence, atau idempotency key.
Versi yang lebih eksplisit
{
"event_id": "evt_7fa2c",
"event_type": "order.payment_captured",
"event_version": "2.0",
"occurred_at": "2026-08-11T10:15:30Z",
"producer": "payment-service",
"aggregate_id": "ord_5001",
"sequence": 12,
"idempotency_key": "order-payment-captured-ord_5001-12",
"data": {
"order_id": "ord_5001",
"payment_id": "pay_7009",
"currency": "IDR",
"gross_amount": 250000,
"net_amount": 245000,
"fee_amount": 5000,
"captured_at": "2026-08-11T10:15:28Z"
}
}Versi kedua lebih aman karena:
- nama event menyampaikan transisi bisnis yang jelas,
- field uang punya makna eksplisit,
- ada metadata untuk dedup, tracing, dan ordering,
- consumer dapat memutuskan logika berdasarkan
event_version.
Checklist review kontrak API dan event
- Apakah nama event cukup spesifik untuk menyampaikan niat bisnis?
- Apakah jelas ini snapshot penuh atau delta perubahan?
- Apakah setiap field wajib benar-benar dibutuhkan untuk interpretasi yang benar?
- Apakah makna
null, field hilang, dan string kosong dibedakan? - Apakah enum diperlakukan sebagai himpunan terbuka dengan fallback aman?
- Apakah versi tersedia di metadata dan didokumentasikan?
- Apakah ada
event_iddanidempotency_keyyang stabil? - Apakah duplicate delivery sudah diasumsikan sebagai hal normal?
- Apakah strategi retry membedakan error sementara dan permanen?
- Apakah ada
aggregate_iddan mekanisme ordering bila dibutuhkan? - Apakah consumer tahu apa yang harus dilakukan saat menerima event lama atau out-of-order?
- Apakah metrik, log, dan trace cukup untuk mendeteksi drift semantik?
- Apakah ada masa transisi untuk kompatibilitas mundur saat kontrak berubah?
- Apakah tersedia jalur replay atau rekonsiliasi bila event hilang?
Kesalahan umum yang sering terjadi
- Menganggap validasi schema saja sudah cukup.
- Menggunakan event generik seperti
updateduntuk banyak makna berbeda. - Menambah enum baru tanpa fallback di consumer.
- Mengandalkan urutan kirim sebagai urutan terima.
- Menyimpan dedup setelah side effect dijalankan.
- Me-retry semua error tanpa klasifikasi.
- Menghapus field lama terlalu cepat sebelum semua consumer migrasi.
Penutup
Kontrak API untuk event semantik yang baik harus dirancang untuk dunia nyata: payload akan berevolusi, event akan dikirim ulang, sebagian akan datang terlambat, dan beberapa consumer akan tertinggal versinya. Karena itu, desain yang aman biasanya mencakup envelope metadata yang stabil, versioning yang eksplisit, field dengan makna jelas, enum yang toleran terhadap nilai baru, idempotensi yang benar-benar diterapkan di storage, retry yang terkontrol, serta observability yang cukup untuk mendeteksi drift semantik.
Jika dua sistem bisa membaca pesan yang sama tetapi menarik kesimpulan berbeda, masalahnya bukan lagi serialisasi, melainkan kontrak. Dan kontrak yang baik selalu menulis makna, bukan hanya bentuk.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!