"Madame Semver Will See You Now" lucu karena terasa akurat: banyak tim memperlakukan semver API seperti ramalan aman. Jika versi hanya naik patch atau minor, integrasi dianggap tidak berisiko. Masalahnya, API yang dipakai sistem lain bukan cuma soal signature endpoint atau URL. Integrasi bergantung pada kontrak data, urutan event, perilaku retry, dan asumsi error handling. Area-area ini sering berubah tanpa dianggap sebagai breaking change, padahal dampaknya nyata di production.
Jawaban singkatnya: SemVer API bukan jaminan aman untuk integrasi. Bahkan tanpa perubahan major version, Anda tetap bisa mengalami data parser rusak, job diproses dua kali, webhook datang dalam urutan berbeda, atau enum baru membuat switch statement gagal. Karena itu, review perubahan API harus fokus pada kontrak perilaku, bukan hanya nomor versi.
Mengapa SemVer Sering Gagal Menjadi Sinyal Aman untuk Integrasi
Semantic Versioning berguna untuk library, terutama ketika konsumen berinteraksi lewat antarmuka kode yang jelas: fungsi, class, package, dan dependency graph. Untuk API terdistribusi, realitasnya lebih berantakan. Consumer tidak hanya bergantung pada bentuk request/response, tetapi juga pada:
- Field opsional yang ternyata diasumsikan selalu ada atau selalu kosong.
- Enum baru yang tidak ditangani parser atau logika bisnis consumer.
- Urutan event webhook yang berubah karena retry, queue, atau paralelisme.
- Retry behavior yang membuat duplikasi side effect.
- Error contract yang bergeser dari format lama ke format baru.
- Timing, eventual consistency, dan perubahan default timeout.
Secara teknis, provider bisa berkata: "Kami tidak menghapus endpoint" atau "field lama masih ada". Namun consumer bisa tetap rusak karena mereka mengandalkan perilaku implisit yang tidak pernah dianggap sebagai kontrak formal. Inilah jebakan terbesar: API compatibility bukan cuma soal shape, tetapi juga soal semantics dan delivery behavior.
Breaking Change yang Sering Tidak Dianggap Breaking
1. Menambah field opsional yang mengganggu parser atau validasi
Secara teori, menambah field opsional pada JSON dianggap backward-compatible. Dalam praktiknya, itu bergantung pada cara consumer membaca payload. Consumer yang melakukan validasi ketat terhadap daftar field, melakukan signature berdasarkan serialisasi mentah, atau memetakan field ke model yang rigid bisa gagal.
Contoh sebelum:
{
"id": "ord_123",
"status": "paid",
"amount": 50000
}Contoh sesudah:
{
"id": "ord_123",
"status": "paid",
"amount": 50000,
"payment_method": {
"type": "bank_transfer"
}
}Bagi provider, ini hanya penambahan field. Bagi consumer, ada beberapa potensi masalah:
- Validator menolak additional properties.
- Hash atau signature internal berubah karena payload mentah berubah.
- Serializer/deserializer custom gagal karena skema lokal kaku.
Pelajaran pentingnya: "opsional" dari sisi provider tidak otomatis "aman" dari sisi consumer.
2. Enum baru yang mematahkan logika bisnis
Menambahkan nilai enum hampir selalu dianggap aman. Padahal banyak consumer menulis logika seperti switch atau if/else yang mengasumsikan daftar nilai tertutup.
Contoh sebelum:
{
"status": "paid"
}Nilai yang didokumentasikan:
pending | paid | failedLalu provider menambahkan:
refundedJika consumer punya kode seperti ini:
function mapStatus(status) {
switch (status) {
case 'pending':
return 'WAITING';
case 'paid':
return 'SUCCESS';
case 'failed':
return 'FAILED';
default:
throw new Error(`Unknown status: ${status}`);
}
}Begitu refunded muncul, integrasi gagal. Ini bukan bug provider semata; ini tanda bahwa enum pada integrasi eksternal sebaiknya diperlakukan sebagai open set, bukan closed set, kecuali kontraknya benar-benar menjamin sebaliknya.
3. Perubahan urutan event webhook
Banyak integrasi diam-diam mengasumsikan webhook datang berurutan: created lalu paid lalu settled. Di sistem terdistribusi, asumsi itu rapuh. Retry, queue terpisah, partitioning, atau worker paralel bisa membuat event datang terlambat, ganda, atau tertukar.
Contoh asumsi yang sering salah:
- Event
invoice.paidpasti datang setelahinvoice.created. - Webhook untuk objek yang sama pasti diproses satu per satu.
- Retry akan mempertahankan urutan asli.
Padahal skenario berikut sangat mungkin terjadi:
invoice.createddikirim.- Consumer timeout, provider menjadwalkan retry.
invoice.paiddikirim dan berhasil diterima lebih dulu.- Retry
invoice.createdbaru masuk belakangan.
Jika consumer membuat state machine yang hanya valid untuk urutan ideal, data menjadi inkonsisten atau job gagal terus.
4. Retry behavior yang berubah tanpa perubahan endpoint
Provider dapat mengubah kebijakan retry dari sisi infrastruktur tanpa mengubah path API. Misalnya:
- Jumlah retry bertambah.
- Backoff menjadi lebih agresif.
- Timeout pengakuan sukses berubah.
- HTTP 5xx, timeout, atau bahkan respons lambat dianggap gagal dan dikirim ulang.
Dampaknya bisa besar jika consumer tidak idempotent. Misalnya webhook payment.succeeded memicu:
- pengiriman email dua kali,
- pencatatan ledger ganda,
- stock berkurang dua kali,
- provisioning akun dilakukan berulang.
Perubahan retry behavior mungkin dianggap "non-breaking" dari sisi provider, tetapi jelas bisa mematahkan integrasi yang mengasumsikan exactly-once delivery.
5. Error contract berubah meski status code tetap
Banyak client tidak hanya memeriksa HTTP status, tetapi juga mem-parsing body error untuk menentukan tindakan. Perubahan format error sering diremehkan.
Contoh sebelum:
{
"error": "invalid_request",
"message": "amount is required"
}Contoh sesudah:
{
"code": "validation_error",
"details": [
{
"field": "amount",
"message": "is required"
}
],
"request_id": "req_abc123"
}Jika client memiliki logika retry atau klasifikasi error berdasarkan field error, perubahan ini memengaruhi kontrol alur, observability, bahkan perilaku queue.
Contoh Kontrak Sebelum dan Sesudah yang Tampak Kecil Tapi Berbahaya
Perubahan respons sinkron
Sebelum:
{
"id": "pay_123",
"status": "pending",
"captured": false
}Sesudah:
{
"id": "pay_123",
"status": "authorized",
"captured": false,
"next_action": "capture_required"
}Di atas, provider mungkin menganggap ini minor enhancement: menambahkan status baru dan field baru. Tetapi consumer yang menganggap hanya ada pending, paid, dan failed akan salah mengelompokkan transaksi.
Perubahan kontrak webhook
Sebelum:
{
"event_id": "evt_1",
"type": "payment.succeeded",
"created_at": "2026-05-10T10:00:00Z",
"data": {
"payment_id": "pay_123",
"order_id": "ord_123"
}
}Sesudah:
{
"event_id": "evt_1",
"type": "payment.updated",
"created_at": "2026-05-10T10:00:00Z",
"sequence": 42,
"data": {
"payment_id": "pay_123",
"order_id": "ord_123",
"status": "succeeded"
}
}Ini tampak lebih generik dan fleksibel. Namun consumer lama yang hanya subscribe atau memfilter payment.succeeded bisa kehilangan event. Bahkan jika event tetap terkirim, interpretasi logika berubah dari event bertipe spesifik ke event bertipe umum dengan state di dalam payload.
Checklist Review Perubahan API yang Lebih Berguna daripada Melihat Nomor Versi
Sebelum menyimpulkan sebuah perubahan aman, review setidaknya area berikut:
- Shape data
Apakah ada field baru, field hilang, field nullable menjadi non-nullable atau sebaliknya, tipe berubah, nama berubah, nesting berubah? - Enum dan state machine
Apakah ada status/event type baru? Apakah transisi state masih sama? - Default behavior
Apakah sorting, pagination, filtering, atau nilai default berubah? - Error contract
Apakah status code, format body, kode error, atau makna retryable vs non-retryable berubah? - Delivery semantics
Untuk webhook/event, apakah ada perubahan urutan, deduplikasi, retry, backoff, atau jaminan at-least-once? - Idempotency
Apakah request yang sama tetap aman diulang? Apakah event duplikat tetap aman diproses? - Temporal assumptions
Apakah ada perubahan timeout, eventual consistency, atau keterlambatan sinkronisasi antarresource? - Backward compatibility untuk consumer nyata
Apakah perubahan aman untuk consumer yang strict, bukan hanya consumer ideal yang tolerant?
Jika sebuah perubahan mengubah hal yang diasumsikan consumer untuk mengambil keputusan, memproses state, atau menghindari duplikasi, anggap itu kandidat breaking change meskipun endpoint dan versi utama tidak berubah.
Strategi Mitigasi: Jangan Percaya SemVer Saja
1. Schema validation yang jelas, tapi jangan salah arah
Schema validation tetap penting, terutama untuk mendeteksi drift kontrak lebih cepat. Namun ada dua jebakan umum:
- Terlalu ketat: menolak field tambahan yang sebenarnya tidak relevan.
- Terlalu longgar: menerima data yang seharusnya wajib untuk alur bisnis.
Pendekatan praktis:
- Validasi field yang benar-benar Anda butuhkan.
- Bedakan field required for parsing dan required for business logic.
- Untuk enum eksternal, sediakan fallback
unknownatau jalur penanganan default yang aman.
Contoh skema yang lebih toleran secara konsep:
{
"type": "object",
"required": ["event_id", "type", "data"],
"properties": {
"event_id": { "type": "string" },
"type": { "type": "string" },
"data": { "type": "object" }
},
"additionalProperties": true
}Intinya bukan menerima semuanya tanpa kontrol, tetapi memvalidasi kontrak minimum yang Anda perlukan untuk aman memproses event.
2. Consumer-driven contract test
Untuk integrasi penting, consumer-driven contract testing membantu karena yang diuji bukan hanya klaim provider, tetapi kebutuhan nyata consumer. Consumer mendefinisikan contoh request/response atau event yang harus tetap didukung; provider memverifikasi bahwa implementasinya masih memenuhi kontrak itu.
Manfaatnya:
- Perubahan yang tampak kecil terlihat berdampak sebelum rilis.
- Asumsi consumer terdokumentasi eksplisit.
- Kompatibilitas diuji otomatis, bukan disimpulkan dari nomor versi.
Keterbatasannya:
- Tidak otomatis menangkap masalah urutan event dan retry jika kontraknya hanya berbasis payload statis.
- Perlu disiplin untuk merawat kontrak dari banyak consumer.
Karena itu, untuk webhook/event-driven integration, contract test sebaiknya dilengkapi dengan skenario delivery behavior.
3. Tolerant reader untuk payload eksternal
Pola tolerant reader berarti consumer hanya bergantung pada bagian kontrak yang memang dibutuhkan, mengabaikan field tambahan, dan tidak rusak hanya karena provider menambahkan metadata baru.
Prinsip implementasi:
- Jangan fail hanya karena ada field tambahan.
- Jangan asumsikan urutan properti JSON.
- Jangan asumsikan enum akan selalu terbatas pada daftar saat ini.
- Jika field tak dikenal muncul, log-kan untuk observasi tanpa langsung memutus alur kritis, kecuali risikonya tinggi.
Trade-off-nya: terlalu toleran bisa menyamarkan perubahan penting. Solusinya adalah toleran saat parsing, ketat saat membuat keputusan bisnis. Misalnya, event dengan field tambahan boleh diterima, tetapi status yang tidak dikenali diarahkan ke antrean review atau ditandai sebagai unknown.
4. Idempotency key untuk request dan deduplikasi untuk webhook
Jika retry mungkin terjadi, anggap duplikasi sebagai kondisi normal. Untuk API request yang membuat side effect, gunakan idempotency key agar pengulangan request yang sama tidak membuat resource ganda.
Contoh request:
POST /payments
Idempotency-Key: 7d4b6a19-5f1d-4ab0-9d3d-1e9d5c2f8e31
Content-Type: application/jsonDi sisi server, simpan hasil request berdasarkan kombinasi idempotency key dan konteks yang relevan. Jika request identik masuk lagi, kembalikan hasil yang sama atau penolakan yang konsisten, bukan membuat side effect baru.
Untuk webhook, gunakan event_id atau kombinasi identifier yang stabil untuk deduplikasi. Contoh pseudo-code:
BEGIN;
INSERT INTO processed_webhooks(event_id, processed_at)
VALUES (:event_id, NOW())
ON CONFLICT (event_id) DO NOTHING;
-- jika tidak ada row yang ter-insert, event sudah pernah diproses
-- hentikan dengan aman
-- jalankan side effect bisnis di transaksi yang sama jika memungkinkan
COMMIT;Poin pentingnya: deduplikasi harus dekat dengan side effect. Jika Anda mengecek dulu lalu menulis kemudian di luar boundary yang aman, race condition masih bisa menghasilkan duplikasi.
5. Versioning untuk webhook dan event, bukan hanya REST endpoint
Banyak tim hanya memikirkan versioning di URL API, misalnya /v1. Padahal perubahan paling sensitif sering terjadi di webhook atau event stream. Jika payload atau semantics event berubah material, pertimbangkan:
- Versi di nama event, misalnya
payment.succeeded.v2. - Versi pada envelope event, misalnya field
schema_version. - Endpoint webhook terpisah per versi consumer.
Pilih pendekatan berdasarkan kebutuhan kompatibilitas:
- Event name versioning cocok jika semantics event memang berubah dan consumer perlu migrasi eksplisit.
- Schema version field cocok jika envelope tetap sama tetapi payload berkembang bertahap.
- Endpoint per versi cocok jika provider perlu mempertahankan perilaku berbeda untuk beberapa consumer lama.
Yang penting, jangan memaksa semua perubahan event masuk ke satu label versi global lalu berharap consumer bisa menebak perbedaannya.
Pola Implementasi yang Lebih Aman untuk Consumer
Bangun state dari fakta terbaru, bukan asumsi urutan
Jika memungkinkan, perlakukan webhook sebagai sinyal bahwa sesuatu berubah, lalu ambil state terbaru dari API sumber. Ini mengurangi ketergantungan pada urutan event.
Trade-off:
- Lebih aman terhadap out-of-order event.
- Menambah latency dan panggilan API.
- Butuh strategi rate limit dan retry yang baik.
Pendekatan ini cocok untuk domain yang sensitif terhadap konsistensi, misalnya pembayaran, provisioning, atau perubahan hak akses.
Pisahkan parsing, validasi, dan side effect
Jangan campur semua logika di satu handler webhook. Struktur yang lebih aman biasanya seperti ini:
- Verifikasi autentikasi/signature.
- Parse envelope dasar.
- Deduplikasi berdasarkan event id.
- Validasi field minimum.
- Simpan event mentah untuk audit/debugging.
- Proses mapping ke domain internal.
- Jalankan side effect idempotent.
Dengan pemisahan ini, perubahan kontrak lebih mudah diisolasi. Anda bisa menerima event baru, menyimpannya, dan menandainya sebagai belum dipahami tanpa langsung merusak pipeline bisnis.
Bedakan error retryable dan non-retryable
Untuk inbound webhook maupun outbound API call, kategorikan error dengan jelas:
- Retryable: timeout, koneksi putus, 5xx, rate limit sementara.
- Non-retryable: payload invalid permanen, signature gagal, resource tidak dikenal karena bug pemetaan.
Kesalahan umum adalah me-retry semua 4xx atau justru tidak me-retry timeout. Keduanya berbahaya. Jika error contract provider berubah, klasifikasi ini bisa ikut rusak. Karena itu, parsing error response harus diuji sebagai bagian dari kontrak, bukan dianggap detail minor.
Debugging Saat Integrasi Tiba-Tiba Rusak Setelah Upgrade "Aman"
Jika setelah bump minor/patch integrasi bermasalah, cek area berikut sebelum menyimpulkan ada bug acak:
- Payload mentah
Bandingkan request/response/webhook sebelum dan sesudah. Cari field baru, nilai enum baru, nullability, atau nesting berbeda. - Urutan dan waktu kedatangan event
Lihat timestamp, retry count, dan korelasi event untuk resource yang sama. - Duplikasi
Pastikan event atau request yang sama tidak diproses dua kali akibat timeout atau retry. - Error body
Periksa apakah status code sama tetapi struktur body berubah. - Asumsi parser
Audit apakah consumer menolak field tambahan, mengandalkan enum tertutup, atau gagal pada unknown value. - Observability
Pastikan log menyimpanrequest_id,event_id, status mapping internal, dan hasil deduplikasi.
Praktiknya, menyimpan payload mentah yang telah diverifikasi sering sangat membantu. Tanpa itu, Anda hanya menebak apakah masalahnya ada pada provider, queue, parser, atau business logic.
Kesimpulan
SemVer API bukan jaminan aman untuk integrasi, terutama saat kontrak yang benar-benar dipakai consumer meliputi lebih dari sekadar endpoint dan field wajib. Minor atau patch release tetap bisa mematahkan sistem lewat field opsional, enum baru, perubahan urutan event webhook, retry behavior, idempotency yang lemah, dan error contract yang bergeser.
Cara yang lebih aman adalah memperlakukan integrasi sebagai kontrak perilaku: validasi skema minimum, uji kontrak dari perspektif consumer, bangun parser yang toleran tetapi keputusan bisnis yang ketat, gunakan idempotency key dan deduplikasi event, serta versioning yang eksplisit untuk webhook/event. Nomor versi masih berguna, tetapi tidak cukup. Yang melindungi production Anda adalah disiplin terhadap kontrak nyata, bukan keyakinan bahwa patch dan minor pasti aman.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!