Pada sistem yang serba terbatas, integrasi gagal bukan pengecualian melainkan kondisi normal yang harus diantisipasi. Inspirasi yang menarik datang dari proyek Linux for the Sega MegaDrive: lingkungan dengan resource ketat memaksa engineer membuat asumsi yang jelas, protokol yang sederhana, dan jalur gagal yang dapat diprediksi. Pelajaran yang sama sangat relevan untuk backend modern, terutama saat API dipanggil oleh banyak client, melewati jaringan yang tidak stabil, dan harus tetap aman terhadap retry, duplikasi, serta perubahan kontrak.

Inti dari kontrak API tangguh bukan sekadar dokumentasi endpoint. Kontrak yang baik mendefinisikan perilaku saat sukses, saat timeout, saat request yang sama terkirim dua kali, saat field berevolusi, dan saat event diterima berulang. Jika hal-hal ini tidak dinyatakan dengan tegas, integrasi menjadi rapuh walaupun endpoint terlihat sederhana.

Mengapa konteks sistem terbatas relevan untuk backend modern

Pada sistem dengan memori, CPU, atau I/O terbatas, kita tidak bisa mengandalkan retry tanpa batas, payload berlebihan, atau asumsi bahwa koneksi selalu sehat. Dalam praktik backend modern, batasannya mungkin bukan RAM cartridge, tetapi:

  • latensi antar layanan yang tidak konsisten,
  • worker queue yang bisa crash di tengah proses,
  • client mobile dengan koneksi buruk,
  • webhook provider yang akan mengirim ulang event,
  • deploy bertahap yang menyebabkan dua versi client hidup bersamaan.

Karena itu, kontrak API perlu didesain seolah-olah lingkungan selalu memiliki gangguan. Prinsipnya sederhana: buat input eksplisit, output stabil, dan kegagalan dapat diulang dengan aman.

Pilar kontrak API tangguh

1. Skema request dan response harus tegas

Semakin longgar definisi payload, semakin besar peluang interpretasi berbeda antar tim. Tetapkan dengan jelas:

  • field wajib dan opsional,
  • tipe data setiap field,
  • format waktu, angka, dan mata uang,
  • nilai enum yang diizinkan,
  • kode error beserta maknanya.

Contoh request pembuatan pembayaran:

POST /v1/payments
Content-Type: application/json
Idempotency-Key: 7f4d2b9a-9d7a-4f3e-b7d2-1bc1c5f7c001

{
  "order_id": "ORD-2026-000123",
  "customer_id": "CUS-9981",
  "amount": {
    "value": 150000,
    "currency": "IDR"
  },
  "payment_method": "bank_transfer",
  "callback_url": "https://merchant.example.com/webhooks/payments"
}

Contoh response sukses:

{
  "payment_id": "PAY-41b8f2",
  "status": "pending",
  "created_at": "2026-08-05T10:15:30Z",
  "amount": {
    "value": 150000,
    "currency": "IDR"
  }
}

Hal penting di sini bukan hanya nama field, tetapi juga stabilitas makna. Misalnya, amount.value harus selalu dalam satuan terkecil yang sudah disepakati atau selalu angka bulat nominal penuh. Jangan biarkan dua interpretasi hidup bersamaan.

2. Operasi write harus idempotent

Pada jaringan yang tidak stabil, client sering tidak tahu apakah request sebelumnya berhasil atau tidak. Jika client mengirim ulang request create tanpa mekanisme idempotency, server dapat membuat resource ganda.

Solusi yang umum dan efektif adalah Idempotency-Key pada operasi write seperti:

  • pembuatan order,
  • pembayaran,
  • refund,
  • pengiriman email atau notifikasi yang dipicu API.

Alur implementasi server:

  1. Client mengirim Idempotency-Key unik untuk satu niat bisnis.
  2. Server menyimpan kombinasi key, identitas pemilik, fingerprint request, status proses, dan response akhir.
  3. Jika request identik datang lagi dengan key yang sama, server mengembalikan response yang sama, bukan membuat operasi baru.
  4. Jika key sama tetapi payload berbeda, server mengembalikan error konflik karena niat request tidak lagi konsisten.

Contoh respons saat key yang sama dipakai dengan payload berbeda:

{
  "error": {
    "code": "IDEMPOTENCY_KEY_REUSED_WITH_DIFFERENT_PAYLOAD",
    "message": "Idempotency-Key sudah pernah dipakai untuk payload yang berbeda"
  }
}

Mengapa ini bekerja? Karena idempotency memindahkan tanggung jawab dari “jangan sampai jaringan retry” menjadi “retry boleh terjadi, hasil tetap satu”. Ini jauh lebih realistis pada sistem produksi.

Catatan: idempotency berbeda dari deduplikasi murni berbasis payload. Dua request dengan payload sama belum tentu mewakili niat bisnis yang sama jika dikirim pada waktu berbeda.

3. Timeout dan retry budget harus menjadi bagian dari kontrak operasional

Banyak integrasi rapuh bukan karena payload salah, tetapi karena retry dilakukan tanpa disiplin. Akibatnya, sistem justru tumbang saat dependency melambat.

Yang perlu ditetapkan:

  • batas waktu koneksi dan baca yang masuk akal,
  • kapan client boleh retry,
  • berapa kali maksimum retry,
  • backoff dan jitter,
  • operasi mana yang aman di-retry.

Panduan praktis:

  • GET biasanya aman di-retry jika server memang menerapkan semantics read-only.
  • POST write hanya aman di-retry bila didukung idempotency key.
  • Jangan retry semua status 4xx. Umumnya 4xx berarti masalah request, bukan gangguan sementara.
  • Retry pada timeout, koneksi putus, atau 5xx tertentu lebih masuk akal, tetapi tetap dengan batas.

Contoh pseudo-config client:

request_timeout_ms = 2000
connect_timeout_ms = 500
max_retries = 2
retry_on = [timeout, connection_reset, 502, 503, 504]
backoff = exponential_with_jitter

Retry budget penting agar satu request bisnis tidak berubah menjadi badai request turunan. Misalnya, jika satu endpoint upstream punya tiga dependency, retry tanpa batas di setiap lapisan dapat memperparah lonjakan beban. Budget memaksa kita memilih: lebih baik gagal cepat dan jelas, atau menunggu lebih lama dengan peluang sukses lebih tinggi.

4. Versioning perubahan field harus konservatif

Integrasi sering patah bukan karena endpoint hilang, tetapi karena perubahan field yang dianggap “sepele”. Menambah field baru memang biasanya aman, tetapi mengganti tipe, mengubah arti field, atau menghapus field lama adalah perubahan besar.

Aturan praktis:

  • Aman: menambah field opsional baru pada response.
  • Berisiko: mengubah tipe dari string ke object, walau nama field tetap sama.
  • Breaking: menghapus field, mengubah format tanggal, mengubah enum tanpa masa transisi, atau membuat field opsional menjadi wajib.

Jika perlu evolusi field, pertimbangkan pola berikut:

  • tambahkan field baru, pertahankan field lama sementara waktu,
  • dokumentasikan deprecation dengan tenggat jelas,
  • hindari overload makna satu field untuk beberapa skenario,
  • gunakan versi endpoint hanya untuk perubahan yang benar-benar breaking.

Contoh evolusi yang aman:

{
  "payment_id": "PAY-41b8f2",
  "status": "pending",
  "status_detail": {
    "code": "AWAITING_TRANSFER",
    "message": "Menunggu transfer bank"
  }
}

Field status lama tetap dipertahankan, sementara detail baru ditambahkan di status_detail. Client lama tetap hidup, client baru mendapat konteks lebih kaya.

5. Duplicate webhook dan event harus dianggap normal

Webhook umumnya memiliki sifat at-least-once delivery: event bisa datang lebih dari sekali, bisa datang terlambat, bahkan bisa datang tidak berurutan. Jika consumer menganggap setiap event unik dan selalu berurutan, bug akan muncul cepat atau lambat.

Kontrak webhook sebaiknya memiliki:

  • event_id unik global,
  • event_type,
  • occurred_at,
  • identitas resource yang berubah,
  • signature atau mekanisme verifikasi asal event.

Contoh payload webhook:

{
  "event_id": "evt_01k2xyz9",
  "event_type": "payment.completed",
  "occurred_at": "2026-08-05T10:18:02Z",
  "data": {
    "payment_id": "PAY-41b8f2",
    "order_id": "ORD-2026-000123",
    "status": "completed"
  }
}

Strategi consumer:

  1. Simpan event_id yang sudah diproses.
  2. Lakukan pemrosesan dalam transaksi atau mekanisme yang mencegah efek samping ganda.
  3. Jika event yang sama datang lagi, balas sukses tetapi lewati pemrosesan ulang.
  4. Jangan bergantung penuh pada urutan kedatangan; ambil state final dari resource bila perlu.

Ini alasan mengapa deduplikasi event berbeda dari idempotency request. Idempotency melindungi sisi producer API untuk write dari client, sedangkan deduplikasi webhook melindungi sisi consumer dari pengiriman ulang event.

Desain kontrak API yang konkret

Format error yang dapat diandalkan

Error response perlu cukup stabil untuk dipakai machine-to-machine. Jangan hanya mengirim string bebas yang berubah-ubah. Format berikut lebih aman:

{
  "error": {
    "code": "INVALID_AMOUNT",
    "message": "amount.value harus lebih besar dari nol",
    "retryable": false,
    "request_id": "req_d7f91a"
  }
}

code dipakai client untuk pengambilan keputusan. message untuk debugging manusia. retryable membantu client yang lebih generik, walau keputusan akhir tetap harus mempertimbangkan konteks bisnis.

Status sinkron versus asinkron

Pada sistem terbatas, jangan paksakan semua pekerjaan selesai dalam satu request. Jika operasi write memicu proses panjang, lebih aman mengembalikan status awal lalu biarkan client memantau hasilnya lewat polling atau webhook.

Contoh:

  • POST /v1/payments mengembalikan status pending.
  • Client kemudian memanggil GET /v1/payments/{id} atau menerima webhook saat status berubah.

Pendekatan ini mengurangi timeout panjang, menurunkan coupling, dan memberi ruang untuk retry yang aman.

Field nullable, opsional, dan default

Salah satu sumber bug integrasi adalah kebingungan antara field tidak ada, field null, dan field kosong. Tentukan semantiknya:

  • tidak ada: data belum dikirim atau tidak relevan,
  • null: data diketahui tetapi memang tidak memiliki nilai,
  • string kosong: jarang tepat untuk data terstruktur.

Jika server memberikan default, dokumentasikan kapan default berlaku. Jangan mengandalkan client menebak-nebak.

Edge case yang wajib dipikirkan sejak awal

Kasus 1: timeout setelah write sebenarnya sukses

Client mengirim POST create payment. Server berhasil menyimpan data, tetapi koneksi putus sebelum response sampai. Client lalu retry.

Tanpa idempotency: payment duplikat bisa tercipta.

Dengan idempotency key: server mengembalikan hasil request pertama.

Kasus 2: webhook terkirim dua kali

Provider mengirim payment.completed, consumer memproses dan menjawab 200. Karena masalah jaringan, provider tidak menerima respons lalu mengirim ulang event yang sama.

Jika consumer tidak menyimpan event_id, sistem bisa mengirim email sukses dua kali atau mem-posting jurnal dua kali.

Kasus 3: dua versi client hidup bersamaan

Web client baru mengerti field status_detail, aplikasi mobile lama hanya mengenal status. Mengganti bentuk status dari string menjadi object akan mematahkan client lama. Menambahkan field baru jauh lebih aman.

Kasus 4: retry lintas lapisan

Gateway retry 2 kali, service A retry 2 kali, service B retry 2 kali. Satu gangguan kecil bisa berubah menjadi banyak request. Di sinilah retry budget dan observabilitas diperlukan agar retry tidak menjadi amplifier kegagalan.

Kasus 5: event datang tidak berurutan

Consumer menerima payment.completed lebih dulu, lalu payment.pending terlambat. Jika consumer selalu menimpa state dengan event terakhir yang diterima, status bisa mundur. Solusinya bisa berupa pembandingan versi state, timestamp yang dipercaya, atau mengambil status final dari API sumber sebelum mengubah state lokal.

Fallback saat client atau jaringan tidak stabil

Fallback bukan berarti menyembunyikan error, melainkan menjaga sistem tetap berguna dengan degradasi yang terkontrol.

Pola fallback yang masuk akal

  • Outbox pattern untuk event keluar agar data bisnis dan publikasi event tidak terpisah secara rawan.
  • Inbox atau dedup store untuk consumer webhook/event.
  • Queue lokal di sisi client atau edge worker saat upstream sementara tidak tersedia.
  • Read-through cache untuk data referensi yang tidak terlalu sensitif terhadap stale data.
  • Polling sebagai cadangan webhook bila notifikasi push tidak dapat diandalkan.

Trade-off-nya perlu dipahami. Queue lokal meningkatkan ketahanan, tetapi menambah kompleksitas observabilitas dan penanganan poison message. Cache menurunkan beban, tetapi berisiko menyajikan state usang. Polling lebih sederhana daripada webhook pada beberapa kasus, tetapi boros jika interval tidak diatur dengan baik.

Kapan memilih webhook, polling, atau keduanya

  • Webhook cocok jika perubahan perlu didorong cepat ke pihak lain dan consumer siap menerima callback publik.
  • Polling cocok sebagai jalur fallback, atau untuk client yang tidak dapat mengekspos endpoint.
  • Kombinasi keduanya paling aman untuk proses kritikal: webhook untuk notifikasi cepat, polling untuk rekonsiliasi.

Checklist review kontrak API

Gunakan daftar ini saat meninjau desain integrasi sebelum masuk produksi:

  1. Apakah field wajib, opsional, nullable, dan default dijelaskan tanpa ambigu?
  2. Apakah operasi write mendukung Idempotency-Key atau mekanisme setara?
  3. Apakah perilaku ketika key yang sama dipakai dengan payload berbeda sudah ditentukan?
  4. Apakah timeout client dan retry policy didokumentasikan?
  5. Apakah ada retry budget untuk mencegah ledakan request?
  6. Apakah error response memiliki code stabil dan mudah diproses mesin?
  7. Apakah webhook memiliki event_id unik dan mekanisme verifikasi?
  8. Apakah consumer diharapkan menangani duplicate dan out-of-order event?
  9. Apakah perubahan field dievaluasi sebagai additive atau breaking?
  10. Apakah ada strategi deprecation dan masa transisi?
  11. Apakah status sinkron dan asinkron dibedakan dengan jelas?
  12. Apakah ada request_id atau correlation id untuk debugging lintas layanan?
  13. Apakah fallback saat dependency lambat atau tidak tersedia sudah didefinisikan?
  14. Apakah ada skenario rekonsiliasi jika webhook gagal total?

Anti-pattern yang sering membuat integrasi rapuh

  • Menganggap retry sebagai urusan client saja. Tanpa idempotency server, retry bisa menciptakan data ganda.
  • Mengubah tipe field tanpa versi atau masa transisi. Ini termasuk breaking change walau nama field tetap sama.
  • Response error hanya berupa string bebas. Sulit diautomasi dan mudah berubah tanpa sengaja.
  • Mengandalkan urutan event. Jaringan tidak menjamin urutan pengiriman lintas sistem.
  • Retry tanpa backoff dan tanpa jitter. Saat upstream bermasalah, semua client memukul ulang secara serempak.
  • POST panjang yang memaksa proses berat selesai sinkron. Lebih rawan timeout dan sulit dipulihkan.
  • Menggunakan payload sebagai satu-satunya kunci deduplikasi. Niat bisnis dan jendela waktu tidak selalu tercermin dari payload.
  • Tidak menyimpan metadata operasional. Tanpa request id, idempotency record, atau event log, debugging menjadi spekulatif.

Tips implementasi dan debugging di produksi

Simpan metadata minimum yang berguna

Untuk operasi write idempotent, simpan setidaknya:

  • idempotency key,
  • fingerprint payload,
  • status pemrosesan,
  • response yang dikembalikan,
  • waktu dibuat dan waktu kedaluwarsa.

Untuk webhook, simpan:

  • event_id,
  • signature verification result,
  • waktu diterima,
  • status pemrosesan,
  • error terakhir jika gagal.

Bedakan error yang bisa dan tidak bisa di-retry

Kesalahan validasi biasanya tidak perlu retry. Timeout, putus koneksi, atau respons upstream sementara gagal sering layak retry. Namun keputusan retry harus dekat dengan pengetahuan domain, bukan murni generik.

Uji skenario gagal, bukan hanya jalur sukses

Pengujian yang bernilai tinggi untuk kontrak API tangguh meliputi:

  • request yang sama dikirim dua kali dengan idempotency key sama,
  • request yang sama dengan key sama tetapi payload berbeda,
  • timeout setelah write sukses,
  • webhook duplicate,
  • webhook out-of-order,
  • client lama terhadap response yang sudah ditambah field baru,
  • dependency lambat sehingga retry budget terpakai.

Jika memungkinkan, buat contract test antar layanan agar perubahan schema tidak lolos diam-diam ke produksi.

Penutup

Pelajaran dari sistem serba terbatas adalah disiplin: jangan berharap lingkungan selalu baik. Dalam integrasi backend modern, disiplin itu diterjemahkan menjadi kontrak API tangguh yang eksplisit tentang idempotency, timeout, retry budget, evolusi field, duplicate event, dan fallback saat jaringan atau client bermasalah.

API yang tangguh bukan API yang tidak pernah gagal, melainkan API yang tetap dapat dipahami dan dipulihkan saat gagal. Jika kontraknya tegas, client boleh retry tanpa takut menggandakan transaksi, consumer boleh menerima webhook berulang tanpa efek samping ganda, dan perubahan schema dapat dilakukan tanpa mematahkan integrasi yang sudah berjalan.