Masalah dan Kasus Penggunaan
Receiver mengubah HTTP request yang tidak tepercaya menjadi event internal yang durable. Bagian yang sulit adalah batasan antara status-status tersebut. 200 OK yang cepat adalah salah jika proses dapat mengalami crash sebelum menyimpan event. Menjalankan alur kerja pembayaran sebelum merespons juga salah karena dependensi yang lambat menyebabkan percobaan ulang provider dan memperbesar beban.
Gunakan asumsi wawancara berikut:
- Lalu lintas puncak adalah 2.000 request per detik. Rata-rata raw body adalah 10 KiB dan maksimumnya adalah 1 MiB, sehingga ingress rata-rata body pada saat puncak adalah sekitar 19,5 MiB/s sebelum header, replikasi, dan overhead penyimpanan.
- Provider mengharapkan respons dalam 2 detik. Target internal kami adalah acknowledgment p99 sebesar 500 ms untuk menjaga ruang cadangan (headroom).
- Pengiriman bersifat setidaknya sekali (at least once) dan tidak berurutan. Provider dapat mengirimkan event logis yang sama secara bersamaan (concurrently), mencobanya lagi nanti, atau mengirimkan status objek yang lebih baru terlebih dahulu.
- Event yang diterima harus bertahan dari crash receiver dan pada akhirnya mencapai status terminal
PROCESSEDatauFAILED. Satu event logis tidak boleh menerapkan mutasi bisnis yang sama dua kali. - Signing secret dirotasi tanpa downtime. Payload mentah dienkripsi dan disimpan selama 30 hari untuk pemulihan dan audit; catatan idempotensi tetap ada setidaknya selama jendela pengiriman ulang yang didokumentasikan oleh provider.
Kasus penggunaan utamanya adalah pembaruan status pembayaran, perubahan siklus hidup langganan, pengembalian dana, sengketa, dan pemberitahuan akun. Rancangan ini harus bekerja di berbagai provider tanpa berpura-pura bahwa header, algoritma tanda tangan, identitas percobaan ulang, atau aturan timestamp mereka identik.
Apa yang Sedang Dievaluasi oleh Pewawancara
Pertama, pewawancara menginginkan kontrak acknowledgment yang tepat. 2xx berarti "receiver ini menerima event secara durable," bukan "setiap efek samping downstream telah selesai." Mengembalikan status berhasil sebelum penulisan yang durable menciptakan kehilangan data secara diam-diam. Mengembalikan status berhasil untuk duplikat yang sudah diterima adalah benar karena provider dapat berhenti mencoba lagi.
Kedua, mereka menginginkan keamanan dalam urutan yang benar. Receiver membatasi method, content type, header, dan ukuran body; mempertahankan raw bytes yang tepat; memverifikasi tanda tangan khusus provider dengan versi secret tepercaya; membandingkan MAC dalam waktu konstan (constant time); dan memeriksa metadata kesegaran bertanda tangan jika provider menyediakannya. Mengurai (parsing) dan melakukan serialisasi ulang JSON sebelum verifikasi dapat mengubah spasi atau urutan kunci dan membatalkan tanda tangan yang sah.
Ketiga, mereka ingin kandidat memisahkan pencegahan replay dari deduplikasi percobaan ulang. Timestamp bertanda tangan menolak request lama yang ditangkap. ID event atau pengiriman provider yang stabil mencegah percobaan ulang yang valid diterapkan dua kali. Beberapa provider menghasilkan timestamp percobaan dan tanda tangan baru untuk setiap percobaan ulang sambil mempertahankan ID event logis. Satu mekanisme tidak dapat secara aman menggantikan yang lain.
Keempat, mereka menginginkan model pemrosesan durable tanpa celah dual-write database-dan-antrean. Baris inbox database dan baris outbox dapat di-commit bersamaan; relay kemudian mempublikasikan pekerjaan. Sebagai alternatif, worker dapat menyewa (lease) baris inbox secara langsung. Kunci unik diberlakukan oleh penyimpanan, bukan oleh pemeriksaan baca-kemudian-tulis yang rentan terhadap duplikat bersamaan.
Terakhir, jawaban yang kuat menangani status yang tidak berurutan, efek samping eksternal, rotasi secret, poisoned event, backpressure, observabilitas, rekonsiliasi, dan crash di setiap batas transaksi.
Pertanyaan Klarifikasi Sebelum Menjawab
- Apa sebenarnya yang dijanjikan oleh
2xx? Di sini artinya tanda tangan telah lolos dan event, atau duplikatnya yang telah diterima sebelumnya, bersifat durable. Ini tidak menjanjikan bahwa panggilan email, buku besar (ledger), atau API provider telah selesai. - Identitas provider apa yang stabil di seluruh percobaan ulang? Setiap adapter harus mendokumentasikan ID event logis, timestamp percobaan, format tanda tangan, dan apakah pengiriman ulang manual mempertahankan ID yang sama. Jangan pernah menurunkan identitas dari hash payload saja.
- Apakah provider menandatangani raw body dan metadata? Adapter mendefinisikan canonical signed bytes. Framework HTTP harus mengekspos body yang belum tersentuh sebelum middleware JSON berjalan.
- Informasi pengurutan apa yang ada? Lebih utamakan versi atau urutan objek yang otoritatif. Waktu pembuatan event adalah bukti yang berguna tetapi tidak otomatis menjadi urutan yang ketat. Jika tidak ada versi, ambil status provider saat ini untuk event yang mengatur status (state-setting).
- Berapa lama pengiriman ulang dapat terjadi? Retensi idempotensi dan overlap secret lama harus mencakup perilaku terdokumentasi provider dan kebijakan replay manual produk. Retensi raw event selama 30 hari dalam masalah ini adalah asumsi produk, bukan aturan vendor universal.
- Kegagalan mana yang harus memicu percobaan ulang? Jika keaslian tidak dapat dipastikan atau penyimpanan durable tidak tersedia, jangan akui (do not acknowledge). Setelah penerimaan durable, gangguan worker tidak boleh mengubah respons HTTP.
- Data apa yang sensitif? Enkripsi raw body, batasi akses, redaksi log, dan tentukan pengecualian penghapusan. Tanda tangan memverifikasi keaslian dan integritas; tanda tangan tidak mengenkripsi payload.
Kerangka Jawaban 30 Detik
"Saya akan mengekspos endpoint HTTPS khusus provider di balik batas ukuran body dan rate limit, menangkap raw bytes yang tepat, dan memverifikasi ID bertanda tangan, timestamp, dan payload dengan secret saat ini atau sebelumnya menggunakan perbandingan waktu konstan. Dalam satu transaksi database, saya akan menyisipkan baris inbox di bawah unique provider-event key dan baris outbox, lalu mengembalikan 2xx; duplikat yang sudah diterima juga mendapatkan 2xx. Relay dan worker memproses secara asinkron dengan lease dan retry. Mutasi bisnis dan penanda proses di-commit bersamaan, sedangkan efek eksternal menggunakan outbox dan kunci idempotensi yang stabil. Untuk pengiriman yang tidak berurutan, saya menggunakan versi objek atau mengambil status terkini yang otoritatif, bukan urutan kedatangan. Saya akan memantau latensi acknowledgment, alasan penolakan, usia inbox, duplikat, dan kegagalan, lalu menguji duplikat bersamaan, overlap secret, tanda tangan usang, event tidak berurutan, dan crash di sekitar setiap commit."
Pendalaman Langkah demi Langkah
Berikan setiap provider sebuah adapter, tetapi pertahankan satu pipeline receiver. Adapter menyediakan jenis event yang diizinkan, ukuran body maksimum, parsing header, canonical signed bytes, algoritma, versi secret tepercaya, kebijakan kesegaran, dan ekstraksi ID event logis. Secret berasal dari secret store terkelola dan di-cache hanya untuk periode terbatas. Suatu request tidak boleh memilih kunci verifikasinya sendiri melalui header yang tidak tepercaya.
Urutan ingress dilakukan secara terencana:
1. Require HTTPS POST; apply endpoint and provider rate limits.
2. Validate bounded headers and Content-Length when present.
3. Read at most 1 MiB into raw bytes; reject overflow while streaming.
4. Parse signature metadata without parsing the JSON body.
5. Verify current and previous trusted secret versions in constant time.
6. Check the signed attempt timestamp against the provider-specific tolerance.
7. Parse the verified body and validate the event envelope and allowed type.
8. Durably accept under a unique logical-event key, then acknowledge.Kesegaran timestamp dan deduplikasi menyelesaikan serangan yang berbeda. Misalkan seorang penyerang menangkap request valid yang ditandatangani. Jendela timestamp bertanda tangan yang ketat memblokir replay setelah jendela tersebut, tetapi request tangkapan yang sama mungkin masih tiba dua kali di dalamnya. Sebaliknya, catatan event-ID memblokir duplikat event logis tetapi tidak dapat membuktikan bahwa timestamp yang tidak ditandatangani masih segar. Provider juga berbeda: percobaan ulang dapat membawa timestamp percobaan baru yang ditandatangani sambil mempertahankan ID event yang sama. Pertahankan kedua pemeriksaan dan jadikan semantik tepatnya milik adapter.
Gunakan inbox sebagai sumber kebenaran (source of truth):
WebhookInbox(
inbox_id, provider, endpoint_id, provider_event_id,
event_type, object_id, object_version, provider_created_at,
received_at, raw_payload_ref, payload_hash, matched_secret_version,
status, attempt_count, next_attempt_at, lease_until, last_error
)
WebhookOutbox(outbox_id, inbox_id, topic, created_at, published_at)
UNIQUE(provider, endpoint_id, provider_event_id)Di dalam satu transaksi database, sisipkan baris inbox dan notifikasi outbox-nya. Jika kunci unik sudah ada, baca status penerimaannya dan kembalikan 2xx tanpa membuat pekerjaan baru. Ini adalah insert atomik, bukan "query lalu insert". Lakukan commit sebelum memberikan pengakuan. Jika database tidak tersedia atau hasil commit tidak diketahui, kembalikan non-2xx yang dapat dicoba lagi; duplikat berikutnya akan memusat (converge) pada baris unik jika commit pertama benar-benar berhasil.
Desain database-plus-outbox menutup celah antara penyimpanan dan antrean. Relay berulang kali mempublikasikan baris outbox yang belum dipublikasikan dan menandainya telah dipublikasikan. Publikasi dapat terjadi dua kali, sehingga konsumen antrean tetap melakukan deduplikasi berdasarkan inbox_id. Implementasi yang lebih sederhana dapat melewati broker dan membiarkan worker mengklaim baris inbox yang jatuh tempo dengan lease, seperti FOR UPDATE SKIP LOCKED. Pilih berdasarkan throughput dan kebutuhan operasional, tetapi pertahankan inbox sebagai batas penerimaan durable dan audit.
Worker mengklaim lease singkat, mengurai event berversi, dan hanya merutekan tipe yang didukung. Untuk mutasi dalam database yang sama, perbarui baris bisnis, catat event yang diproses, dan tandai inbox sebagai PROCESSED dalam satu transaksi. Tabel event yang diproses memiliki kunci provider stabil yang sama, sehingga percobaan ulang worker menjadi no-op. Untuk panggilan ke layanan lain, tulis entri outbox lokal dengan inbox_id sebagai kunci idempotensinya. Pengiriman tepat sekali (exactly-once) melintasi jaringan arbitrer tetap mustahil; identitas yang stabil dan receiver yang idempoten membuat eksekusi setidaknya sekali (at-least-once) menjadi aman.
Urutan kedatangan tidak dapat mendefinisikan urutan bisnis. Jika event membawa versi objek yang otoritatif, perbarui dengan kondisi seperti incoming_version > stored_version; event yang usang menjadi no-op yang diproses. Jika hanya pemberitahuan pengatur status yang ada, ambil resource provider saat ini dan satukan status lokal. Jika event mewakili delta yang tidak dapat diulang, wajibkan sequence, buat buffer untuk gap terbatas, dan rekonsiliasi versi yang hilang. Timestamp saja dapat seri, mengalami clock skew, atau menggambarkan waktu pembuatan alih-alih urutan commit.
Kegagalan terbagi pada batas durable. Sebelum penerimaan, tanda tangan yang tidak valid, timestamp bertanda tangan yang usang, body yang terlalu besar, secret yang tidak tersedia, dan penyimpanan yang tidak tersedia menghasilkan penolakan atau respons yang dapat dicoba lagi sesuai kebijakan. Jangan menyimpan body sensitif yang belum diverifikasi hanya untuk men-debug-nya. Setelah penerimaan, gangguan antrean atau worker tetap mendapatkan 2xx; catatan inbox terakumulasi dan pemulihan akan mengosongkannya. Kegagalan worker sementara melakukan backoff dengan jitter. Kesalahan skema dan percobaan ulang yang habis masuk ke FAILED, mempertahankan diagnostik yang diredaksi, dan memicu jalur pemulihan yang terlihat oleh operator.
Rotasi secret menjaga versi saat ini dan sebelumnya tetap tepercaya untuk overlap terbatas yang diturunkan dari perilaku pengiriman ulang provider. Catat versi mana yang cocok, tetapi jangan pernah mencatat secret atau tanda tangan ke log. Secret baru dikonfigurasi di kedua ujungnya, diamati dalam produksi, dan versi lama dihentikan (retired) secara eksplisit. Kompromi darurat mungkin memerlukan penghentian segera dan replay dari provider, sehingga runbook rotasi harus membedakan overlap terencana dari respons insiden.
Pada 2.000 request/s dan rata-rata 10 KiB, ingress melihat sekitar 19,5 MiB/s raw body. Perencanaan kapasitas mencakup CPU TLS dan HMAC, laju transaksi database, replikasi, amplifikasi antrean, dan durasi burst. Skalakan ingress stateless secara horizontal, partisi indeks inbox berdasarkan provider dan waktu jika diperlukan, jaga agar kunci unik dapat diberlakukan secara global di dalam batas kepemilikannya, dan letakkan raw body terenkripsi dalam penyimpanan objek jika baris database menjadi terlalu besar.
Ukur laju accepted, duplicate, invalid-signature, stale, oversized, dan unsupported-event secara terpisah. Lacak p50/p95/p99 acknowledgment, latensi commit database, usia inbox tertua yang belum diproses, tingkat keberhasilan dan percobaan ulang worker, jumlah FAILED, lag outbox relay, dan versi secret yang cocok. Peringatan (alerts) harus menggunakan lag dan status durable, bukan kedalaman antrean saja. Pekerjaan rekonsiliasi membandingkan baris inbox yang diterima dengan catatan yang diproses dan publikasi outbox, lalu memasukkan kembali pekerjaan hilang yang aman ke dalam antrean.
Uji dari raw HTTP bytes ke dalam. Gunakan vektor tanda tangan yang dipublikasikan provider, lalu ubah satu byte, spasi, ID, atau timestamp. Uji header yang hilang dan duplikat, batas ukuran body, clock skew, secret saat ini/sebelumnya, dan penghentian. Kirim ratusan salinan bersamaan dari satu event dan buktikan satu baris inbox dan satu mutasi bisnis. Lakukan crash setelah commit inbox tetapi sebelum respons, setelah publish ke antrean tetapi sebelum menandai outbox, dan setelah commit bisnis tetapi sebelum acknowledgment worker. Kirimkan versi 3, 1, dan 2; jenuhkan worker; pulihkan mereka; dan buktikan konvergensi akhir dengan latensi acknowledgment yang terbatas.
Contoh Jawaban Berkualitas Tinggi
"Saya mendefinisikan 2xx sebagai penerimaan durable. Ingress bersifat stateless dan khusus provider hanya pada batas adapter. Ingress menerima HTTPS POST, membatasi body hingga 1 MiB, mempertahankan raw bytes yang tepat, dan memverifikasi ID canonical bertanda tangan provider, timestamp percobaan, dan payload terhadap secret saat ini atau sebelumnya yang tepercaya. Perbandingan HMAC dilakukan dalam waktu konstan. Saya kemudian mengurai envelope yang diverifikasi dan hanya mengizinkan tipe event yang didukung.
Dalam satu transaksi database saya menyisipkan WebhookInbox di bawah UNIQUE(provider, endpoint_id, provider_event_id) dan menyisipkan baris outbox. Saya memberikan acknowledgment hanya setelah commit. Duplikat bersamaan akan mengalami konflik pada kunci tersebut dan juga menerima 2xx tanpa pekerjaan baru. Pada 2.000 request per detik dan rata-rata 10 KiB, raw ingress adalah sekitar 19,5 MiB/s, jadi saya menskalakan ingress secara horizontal dan mengukur kapasitas CPU tanda tangan, commit database, replikasi, dan burst storage daripada hanya menghitung jumlah request.
Outbox relay mempublikasikan inbox_id; publikasi duplikat aman. Worker menyewa (lease) catatan inbox. Mutasi bisnis, penanda processed-event, dan penyelesaian inbox berbagi satu transaksi jika memungkinkan. Efek samping jarak jauh menggunakan outbox lain dan inbox_id sebagai kunci idempotensi. Ini menghindari klaim exactly-once melintasi jaringan sambil memastikan percobaan ulang tidak mengulangi efek logis.
Saya tidak menggunakan urutan kedatangan. Versi objek yang otoritatif membatasi pembaruan; jika tidak, webhook pengatur status memicu pembacaan status provider saat ini. Celah urutan yang hilang masuk ke rekonsiliasi. Sebelum penerimaan durable, request yang tidak dapat diverifikasi atau pemadaman penyimpanan tidak diakui. Setelah penerimaan, gangguan worker diserap oleh inbox dan tetap menerima 2xx.
Saya merotasi secret dengan overlap saat ini/sebelumnya yang terbatas dan mencatat versi yang cocok. Saya memantau latensi acknowledgment, kelas penolakan, duplikat, usia inbox, lag outbox, kegagalan, dan penggunaan versi secret. Terakhir, saya menguji vektor tanda tangan resmi, mutasi byte, timestamp usang, overlap secret, duplikat bersamaan, versi yang tidak berurutan, dan crash sebelum dan sesudah setiap commit. Kondisi kelulusan adalah satu baris durable dan satu mutasi bisnis per event logis, tidak ada kehilangan yang diakui, dan konvergensi akhir setelah pemulihan."
Kesalahan Umum
- Mengurai JSON sebelum verifikasi → serialisasi ulang mengubah byte yang ditandatangani → tangkap dan verifikasi raw body yang tepat terlebih dahulu.
- Mengembalikan
200sebelum persistensi → crash secara diam-diam menghilangkan event yang diakui → commit inbox sebelum merespons. - Menyimpan ke database lalu mempublikasikan sekali → crash di antara penulisan menelantarkan pekerjaan → commit outbox bersama dengan inbox atau sewa baris inbox secara langsung.
- Memeriksa duplikat dengan pembacaan sebelumnya → request bersamaan keduanya lolos → berlakukan unique provider-event key secara atomik.
- Hanya menggunakan kesegaran timestamp → duplikat simultan tetap berlaku dua kali → gabungkan kesegaran dengan deduplikasi stable-ID.
- Hanya menggunakan ID event → request valid yang ditangkap dapat di-replay saat catatannya tidak ada atau kedaluwarsa → verifikasi juga kesegaran bertanda tangan sesuai dengan kontrak provider.
- Mengasumsikan urutan kedatangan adalah urutan event → event yang terlambat memundurkan status → gunakan versi, transisi monotonik, atau rekonsiliasi status otoritatif.
- Melakukan efek samping jarak jauh di handler HTTP → latensi menyebabkan percobaan ulang dan hasil yang tidak diketahui → terima secara durable, lalu gunakan pekerjaan idempoten asinkron.
- Mencatat payload lengkap dan tanda tangan ke log → observabilitas menjadi kebocoran data dan secret → simpan bukti terenkripsi dengan akses terbatas dan catat pengenal yang diredaksi ke log.
Pertanyaan Lanjutan dan Tanggapan
Pertanyaan Lanjutan 1: Mengapa mengembalikan 2xx untuk duplikat yang belum selesai diproses?
Event asli telah diterima secara durable, sehingga percobaan ulang provider lainnya tidak menambah nilai pemulihan. Mengembalikan kegagalan akan menciptakan lebih banyak lalu lintas duplikat. Baris inbox yang ada tetap memenuhi syarat untuk worker dan rekonsiliasi. Ini aman hanya jika baris tersebut durable dan tidak dalam status yang berarti penerimaan di-rollback.
Pertanyaan Lanjutan 2: Bagaimana jika proses mengalami crash setelah melakukan commit inbox tetapi sebelum mengirim 2xx?
Provider mencoba lagi. Kunci unik menemukan baris yang di-commit, tidak ada pekerjaan kedua yang dibuat, dan receiver mengembalikan 2xx. Ini adalah jalur at-least-once yang diharapkan. Jika klien menerima 2xx tetapi hasil koneksi ambigu bagi provider, konvergensi yang sama berlaku.
Pertanyaan Lanjutan 3: Bisakah Redis menyimpan kunci idempotensi?
Redis dapat menjadi akselerator, tetapi cache berumur pendek saja lebih lemah daripada kontrak penerimaan. Eviksi, failover, atau kedaluwarsa dapat memungkinkan event pembayaran yang sama diterapkan lagi. Simpan identitas pemrosesan yang durable untuk jendela bisnis dan pengiriman ulang yang diperlukan; gunakan Redis hanya jika kehilangan kunci tidak dapat melanggar kontrak tersebut.
Pertanyaan Lanjutan 4: Bagaimana Anda menangani downtime secret store?
Gunakan cache in-memory terenkripsi yang terbatas dari versi yang sudah tepercaya dengan masa kedaluwarsa dan metrik yang eksplisit. Jika tidak ada kunci cache yang valid, lakukan fail closed dan kembalikan respons yang dapat dicoba lagi sehingga provider dapat mengirimkan ulang. Jangan pernah menerima pekerjaan yang tidak ditandatangani atau mengambil pengenal kunci dari request yang tidak tepercaya dan mempercayainya secara otomatis.
Pertanyaan Lanjutan 5: Bagaimana Anda memulihkan aliran pembayaran yang tidak berurutan?
Lebih utamakan versi objek provider atau transisi domain monotonik dan tolak regresi status. Jika event hanya menyatakan bahwa suatu objek telah berubah, ambil status otoritatifnya saat ini. Untuk delta berurutan, buat buffer untuk gap yang terbatas, minta versi yang hilang, dan beri peringatan jika gap melebihi jendela pemulihan. Jangan mengurutkan hanya berdasarkan waktu penerimaan lokal.
Pertanyaan Lanjutan 6: Mengapa ini masih bukan exactly-once?
Receiver dapat membuat mutasi lokal dan penanda pemrosesannya bersifat atomik. Receiver tidak dapat melakukan commit secara atomik dengan layanan email, bank, atau provider jarak jauh yang arbitrer. Kegagalan jaringan dapat menyembunyikan apakah pihak jarak jauh telah menerapkan request tersebut. Kunci idempotensi yang stabil, outbox transaksional, percobaan ulang, dan rekonsiliasi memberikan hasil bisnis yang efektif sekali (effectively-once) jika API jarak jauh mendukung idempotensi, sementara kontrak transportasi tetap at-least-once.