Prompt dan skop
Reka bentuk perkhidmatan yang menguruskan langganan berulang untuk produk SaaS, media, atau keahlian. Pelanggan boleh memilih pelan bulanan atau tahunan, bertukar daripada percubaan kepada akses berbayar, menukar pelan di pertengahan kitaran, dan memulihkan akses daripada pembaharuan yang gagal. Sistem mesti menghasilkan invois yang boleh diaudit, memberitahu produk tentang kelayakan (entitlements), dan memproses peristiwa tak segerak (asynchronous events) daripada penyedia pembayaran.
Bahan temu duga awam membentangkan "reka bentuk sistem pengebilan langganan" sebagai soalan reka bentuk sistem mengenai peristiwa subscription-created dan trial-expired, kekangan (bottlenecks), metrik pelanggan, dan pemantauan sistem. Dokumentasi Stripe meletakkan langganan, invois, PaymentIntents, percubaan, prorata, pemulihan hasil, dan webhook dalam kitaran hayat yang sama. Artikel ini tidak mendakwa bahawa syarikat tertentu sentiasa bertanyakan soalan ini.
Andaikan terdapat 1 juta langganan aktif, purata satu peristiwa berkaitan pengebilan bagi setiap langganan setiap hari, puncak pembaharuan sehingga 10x ganda trafik normal, jumlah disimpan dalam unit mata wang terkecil, dan penyedia yang boleh mengalami tamat masa (timeout), menduplikasi peristiwa, atau menghantar peristiwa selepas keadaan tempatan berubah. Sistem mesti mengelakkan caj pendua, memastikan invois boleh dikesan, dan menumpukan keadaan kelayakan selepas percubaan semula.
Perkara yang dinilai oleh penemu duga
Jawapan yang kukuh memisahkan pelan, langganan, invois, percubaan pembayaran, dan kelayakan. Menulis paid=true ke dalam baris pelanggan tidak dapat menyatakan prorata, tempoh tangguh (grace periods), bayaran balik (refunds), atau beberapa percubaan untuk satu invois.
Isyarat seterusnya ialah sempadan keidempotenan (idempotency boundary) yang jelas. Penciptaan invois, panggilan pembayaran, pengendalian webhook, dan penghantaran kelayakan semuanya boleh dicuba semula. Baris gilir (queue) sahaja tidak menghalang caj kedua atau kebenaran kedua.
Penemu duga juga menguji semantik masa dan perakaunan: billing anchor, akhir percubaan, zon masa, bulan lompat, naik taraf serta-merta, pembatalan akhir tempoh, dan jumlah yang tidak boleh diubah (immutable). Invois sejarah tidak boleh berubah apabila harga pelan berubah.
Akhir sekali, calon harus menerangkan lonjakan puncak, pembayaran gagal, peristiwa tidak mengikut urutan, bayaran balik manual, jurang penyelarasan, dan pembaikan. Hasil yang berguna adalah mengetahui perkara yang sepatutnya dicaj, perkara yang telah dicaj, dan kelayakan mana yang sah selepas pemulihan.
Soalan penjelasan sebelum menjawab
- Apakah billing anchor? Bulan kalendar atau tarikh bergolek (rolling date)? Ini mengubah pengiraan akhir tempoh dan pengendalian Februari atau hari ke-31.
- Bilakah caj naik taraf dikenakan? Serta-merta, tempoh seterusnya, atau mengikut pilihan pelanggan? Ini mengubah prorata dan keadaan pending-payment.
- Bagaimanakah penurunan taraf berfungsi? Bayaran balik serta-merta, kesan tempoh seterusnya, atau kredit akaun? Ini mengubah item baris invois dan pengendalian bayaran balik.
- Bilakah pembayaran yang gagal membatalkan akses? Tempoh tangguh baca sahaja dan penggantungan serta-merta mempunyai peraturan kelayakan yang berbeza.
- Adakah cukai, diskaun, atau caj penggunaan dalam skop? Jika ya, input penetapan harga mesti diberi versi sebelum pemuktamadan invois.
- Apakah punca kebenaran (source of truth) kelayakan? Produk boleh menggunakan peristiwa penyedia secara langsung, atau sistem pengebilan boleh menerbitkan peristiwa
entitlement.changed. Pilihan ini mengubah main semula (replay) dan ketekalan. - Adakah pelbagai mata wang, bayaran balik, atau pelarasan manual diperlukan? Jika ya, gunakan unit minor integer dan jangan sekali-kali menulis ganti invois sejarah.
Jawapan 30 saat
"Saya akan memisahkan pelan, langganan, invois, percubaan pembayaran, dan kelayakan. Mesin keadaan langganan menentukan tempoh semasa dan tindakan seterusnya; enjin pengebilan mencipta snapshot invois yang tidak boleh diubah pada setiap sempadan dan menggunakan invoice_id sebagai kunci keidempotenan pembayaran. Sahkan pembayaran melalui webhook yang ditandatangani serta pertanyaan penyedia, nyahduplikasi mengikut ID peristiwa, dan benarkan peristiwa tiba di luar urutan. Wakilkan naik taraf dan turun taraf sebagai item baris invois positif dan negatif yang jelas. Pembayaran yang gagal memasuki mesin keadaan percubaan semula yang terikat dan ber-jitter, dan kelayakan mengikut keadaan pembayaran yang disahkan serta dasar tempoh tangguh yang didokumenkan. Saya akan membuktikan reka bentuk ini dengan penyelarasan, invarian keadaan, dan suntikan kegagalan."
Penyelesaian langkah demi langkah
Langkah 1: Tentukan objek dan invarian.
Plan(id, version, currency, interval, unit_amount, trial_days)
Subscription(id, customer_id, plan_version, status, period_start, period_end)
Invoice(id, subscription_id, period_start, period_end, currency, total, status)
InvoiceLine(id, invoice_id, source, description, quantity, unit_amount, amount)
PaymentAttempt(id, invoice_id, attempt_no, provider_key, status, provider_id)
Entitlement(id, customer_id, feature, valid_until, source_invoice_id)Pelan mungkin berubah, tetapi invois yang dikeluarkan menyimpan versi harga dan snapshot mata wangnya. Invarian utama ialah: tempoh langganan mempunyai paling banyak satu invois yang berkuat kuasa; satu hasil penyedia memajukan invois sekali; kegagalan yang lewat tidak boleh menulis ganti pembayaran yang disahkan; dan memainkan semula perubahan kelayakan bertumpu kepada satu versi.
Langkah 2: Pacu kerja dengan mesin keadaan.
trialing --trial_end--> active
active --renewal--> invoice_open
invoice_open --payment_succeeded--> paid
invoice_open --payment_failed--> past_due
past_due --retry_succeeded--> paid
past_due --retries_exhausted--> canceled
active --cancel_at_period_end--> canceling
canceling --period_end--> canceledArahan atau peristiwa dengan versi melakukan peralihan; permintaan web sewenang-wenangnya tidak seharusnya mengubah status secara langsung. Tentukan dasar kelayakan untuk active, past_due, dan canceled: contohnya, past_due mungkin mengekalkan akses baca sahaja selama tiga hari manakala canceled membuang ciri berbayar. Rekod sebab, peristiwa sumber, pelaku, dan masa untuk setiap peralihan.
Langkah 3: Jana invois dan kendalikan perubahan pelan.
Bahagikan pekerja pengebilan mengikut period_end. Mula-mula cipta invois di bawah kekangan unik seperti (subscription_id, period_start), kemudian kira item barisnya. Untuk naik taraf pada hari ke-15, rekod pelan lama yang tidak digunakan sebagai baris negatif dan baki pelan baharu sebagai baris positif. Gunakan unit minor integer dan peraturan saat-atau-hari yang jelas; jangan sekali-kali menolak nilai titik terapung (floating-point) yang dibundarkan.
Bekukan jumlah, cukai, diskaun, snapshot penggunaan, dan versi pelan apabila invois dimuktamadkan. Kerja tempoh boleh dijalankan dua kali: kekangan unik mengembalikan invois asal, dan pekerja menyemak sama ada percubaan pembayaran sudah wujud. Pembatalan akhir tempoh mengubah cancel_at_period_end; ia tidak memadamkan sejarah invois. Pembatalan serta-merta mencipta pelarasan bayaran balik atau kredit.
Langkah 4: Asingkan keidempotenan pembayaran daripada tamat masa rangkaian.
provider_key = "invoice:" + invoice_id + ":attempt:" + attempt_no
POST payment-provider/charges
Idempotency-Key: provider_keyTulis PaymentAttempt tempatan sebelum memanggil penyedia. Selepas tamat masa, jangan simpulkan kegagalan; cuba semula dengan kunci yang sama atau buat pertanyaan kepada penyedia. Stripe mendokumenkan bahawa permintaan idempoten yang berulang mengembalikan hasil pertama dan membandingkan parameter untuk mengelakkan penggunaan semula kunci secara tidak sengaja. Oleh itu, kunci mesti mewakili satu operasi perniagaan, bukan satu percubaan rangkaian.
Jika berjaya, webhook atau pertanyaan penyedia merekodkan provider_id, jumlah, mata wang, dan masa siap. Kemas kini pangkalan data bersyarat hanya membenarkan open atau past_due beralih kepada paid; payment_failed yang lewat dimasukkan ke dalam rekod audit dan tidak boleh mengundurkan pembayaran yang disahkan. Ketidakpadanan wang atau mata wang memasuki penyelarasan dan bukannya memberikan atau membatalkan akses secara automatik.
Langkah 5: Kendalikan pendua webhook dan penyusunan semula dengan inbox.
WebhookInbox(event_id PRIMARY KEY, received_at, payload_hash, processed_at)
Outbox(id, aggregate_id, event_type, payload, published_at)Sahkan tandatangan, simpan peristiwa mentah dan cincangan (hash) dalam WebhookInbox, dan perakui event_id pendua. Pemproses menggunakan versi objek, status penyedia, dan keadaan invois tempatan untuk memutuskan sama ada untuk maju; masa ketibaan bukan jaminan urutan. Stripe mengesyorkan webhook langganan untuk aktiviti tak segerak dan perubahan kelayakan, jadi pemprosesan dalaman juga mesti boleh dimainkan semula.
Transaksi tempatan boleh menulis keadaan pengebilan, niat perubahan kelayakan, dan rekod Outbox bersama-sama. Penerbit kemudian menghantar rekod tersebut kepada perkhidmatan kelayakan. Pengguna menyahduplikasi mengikut (aggregate_id, version), jadi percubaan semula penyedia, percubaan semula baris gilir, dan permulaan semula pengguna tidak mencipta kebenaran kedua yang berkuat kuasa.
Langkah 6: Anggarkan kapasiti dan asingkan waktu puncak.
Dengan 1 juta langganan aktif dan satu peristiwa pengebilan setiap langganan setiap hari, kadar stabil adalah kira-kira 11.6 peristiwa/saat; puncak pembaharuan 10x adalah kira-kira 116 peristiwa/saat. Beban yang lebih sukar ialah item baris invois, panggilan pembayaran, webhook, dan pemberitahuan yang serentak. Kumpulkan kerja tempoh mengikut masa (bucket), hadkan setiap kelompok, dan tambah jitter untuk mengelakkan lonjakan pada awal jam.
| Beban Kerja | Kekangan utama | Kawalan |
|---|---|---|
| Imbasan tempoh | Indeks panas dan perbalahan kunci (lock contention) | Kumpulkan mengikut period_end, transaksi pendek, kekangan unik |
| Panggilan pembayaran | Kependaman dan had penyedia | KonkuDynamic terikat, kunci keidempotenan, pemunduran eksponen (exponential backoff) |
| Webhook | Peristiwa pendua dan tidak mengikut urutan | Nyahduplikasi inbox, kemas kini versi bersyarat, baris gilir main semula |
| Penyegerakan kelayakan | Tunggakan hiliran (downstream backlog) | Outbox, kelambatan pengguna, kuota penyewa |
Indekskan langganan mengikut ID langganan dan akhir tempoh; jadikan kerja pembayaran dan pemberitahuan tidak segerak. Berikan kuota berasingan kepada penyewa besar, hari pembaharuan yang tertumpu, dan akaun penggunaan tinggi supaya satu penyewa tidak menggunakan semua konkurensi pembayaran. Nombor-nombor ini merupakan andaian permulaan; had penyedia dan ujian kegagalan mesti menentukur reka bentuk.
Langkah 7: Selaraskan, baiki, dan pantau.
Lakukan penyelarasan setiap hari terhadap invois, transaksi penyedia, dan eksport penyelesaian (settlement exports). Untuk setiap invois, semak bahawa item baris berjumlah sama dengan jumlah keseluruhan, jumlah pembayaran sama dengan jumlah yang perlu dibayar, dan tamat tempoh kelayakan tidak melebihi masa berbayar yang disahkan. Cipta rekod pelarasan atau bayaran balik yang tidak boleh diubah untuk perbezaan; jangan sekali-kali menulis ganti jumlah lama.
Jejaki kelambatan imbasan tempoh, kependaman invois, hasil pembayaran yang tidak diketahui, kiraan percubaan semula, usia past_due, kadar pendua webhook dan kelambatan pemprosesan, kelewatan penyebaran kelayakan, perbezaan nilai penyelarasan, dan kegagalan pembayaran bagi setiap penyewa. Rekod audit harus memautkan subscription_id, invoice_id, payment_attempt_id, ID peristiwa, dan ID jejak supaya aduan boleh dikesan kembali kepada respons penyedia.
Langkah 8: Buktikan sempadan dengan matriks kegagalan.
Suntik kerja tempoh yang berulang; ranap sistem selepas penciptaan invois; caj penyedia diikuti dengan respons yang hilang; webhook pendua dan di luar urutan; pembayaran prorata yang gagal; percubaan berakhir tanpa kaedah pembayaran; pembatalan dan pembaharuan serentak; ranap sistem pengguna selepas penerbitan Outbox; dan perlumbaan bayaran balik manual dengan percubaan semula automatik.
Semakan penerimaan ialah: satu invois berkesan bagi setiap (subscription_id, period_start); tiada caj kedua untuk satu provider_key; pembayaran yang disahkan tidak ditulis ganti oleh kegagalan yang lewat; peristiwa kelayakan yang dimainkan semula menghasilkan versi yang sama; setiap perbezaan penyelarasan mempunyai pelarasan yang boleh dikesan; dan setiap tindakan automatik boleh dibina semula daripada rekod audit.
Contoh jawapan berkualiti tinggi
"Saya akan memisahkan pelan, langganan, invois, percubaan pembayaran, dan kelayakan, serta mengambil snapshot harga pelan apabila invois dicipta. Mesin keadaan langganan mengendalikan percubaan, pembaharuan, tempoh tangguh, dan pembatalan. Pekerja tempoh mencipta satu invois di bawah kunci unik; perubahan pelan menjadi item baris invois positif dan negatif menggunakan unit minor integer dan peraturan prorata yang jelas.
Sebelum memanggil penyedia, saya menulis percubaan dan memperoleh kunci keidempotenan invoice_id + attempt_no yang stabil. Tamat masa menggunakan kunci yang sama atau pertanyaan penyedia; ia tidak sekali-kali mencipta kunci baharu. Webhook disahkan, disimpan dalam inbox, dan dinyahduplikasi mengikut ID peristiwa. Kemas kini versi bersyarat menghalang kegagalan di luar urutan daripada menulis ganti pembayaran yang disahkan. Outbox menghantar niat kelayakan ke hiliran secara andal.
Saya mengasingkan konkurensi pembayaran, imbasan tempoh, webhook, pemberitahuan, dan kuota penyewa, serta menambah jitter pada lonjakan pembaharuan. Penyelarasan harian membandingkan invois tempatan, transaksi penyedia, dan penyelesaian. Saya akan menguji kerja tempoh pendua, caj yang berjaya dengan respons yang hilang, webhook yang diubah susunannya, prorata yang gagal, pembatalan dan pembaharuan serentak, serta penumpuan kelayakan selepas main semula."
Kesilapan biasa
- Kesilapan: hanya menyimpan
paidpada langganan → Sebab ia gagal: invois, percubaan, tempoh tangguh, dan bayaran balik hilang → Pembetulan: modelkan invois, percubaan pembayaran, dan kelayakan secara berasingan. - Kesilapan: mencipta ID pembayaran baharu untuk setiap percubaan semula tamat masa → Sebab ia gagal: satu caj perniagaan boleh dilaksanakan dua kali → Pembetulan: simpan satu kunci keidempotenan bagi setiap operasi dan buat pertanyaan bagi hasil yang tidak diketahui.
- Kesilapan: menggunakan peristiwa mengikut masa ketibaan → Sebab ia gagal: webhook boleh diduplikasi, disusun semula, atau ditangguhkan → Pembetulan: simpan inbox dan gunakan versi objek serta peralihan bersyarat.
- Kesilapan: mengira semula invois lama daripada harga pelan hari ini → Sebab ia gagal: perubahan harga menulis semula sejarah → Pembetulan: bekukan harga, cukai, diskaun, penggunaan, dan versi pelan pada invois.
- Kesilapan: membatalkan serta-merta selepas satu kegagalan → Sebab ia gagal: gangguan penyedia sementara menjadi gangguan kelayakan → Pembetulan: gunakan dasar tempoh tangguh
past_duedan mesin keadaan dunning yang terikat. - Kesilapan: memadamkan rekod tempoh semasa pembatalan → Sebab ia gagal: audit, bayaran balik, dan penyelarasan kehilangan bukti → Pembetulan: tambah rekod pembatalan dan pelarasan sambil mengekalkan sejarah.
- Kesilapan: menganggap transaksi pangkalan data sebagai transaksi pembayaran → Sebab ia gagal: penyedia berada di luar transaksi tempatan → Pembetulan: lengkapkan kitaran dengan panggilan idempoten, webhook, pertanyaan, dan penyelarasan.
- Kesilapan: mengimbas setiap langganan pada satu masa yang tepat sama → Sebab ia gagal: kunci pangkalan data, panggilan pembayaran, dan pemberitahuan melonjak serentak → Pembetulan: kumpulkan mengikut kelompok (bucket), tambah jitter, gunakan baris gilir, dan tetapkan kuota kerja.
Soalan susulan dan jawapan
Susulan 1: Pembayaran naik taraf gagal. Apakah yang berlaku kepada akses?
Kekalkan kelayakan lama, cipta pending_update dan invois baharu, dan tukar pelan hanya selepas pembayaran disahkan. Jika produk membenarkan naik taraf sebelum kutipan, tandakan kelayakan baharu sebagai dihadkan tempoh tangguh dengan tamat tempoh yang jelas. Dalam kedua-dua dasar, versi invois dan kelayakan kekal boleh dikesan; menukar plan_id sahaja tidak mencukupi.
Susulan 2: Tiada webhook penyedia tiba selama tiga hari. Bagaimanakah anda mengesannya?
Jalankan kerja penyelarasan daripada keadaan invois tempatan dan pertanyaan penyedia. Berikan amaran pada open lama, past_due, atau invois yang telah tamat tempoh tanpa peristiwa terminal; jika pertanyaan masih tidak diketahui, jedakan pembatalan automatik dan salurkan kepada semakan manual. Webhook ialah pemberitahuan berkependaman rendah, bukan satu-satunya punca kebenaran.
Susulan 3: Pembatalan dan pembaharuan tiba secara serentak. Mana yang menang?
Tetapkan versi monotonik pada arahan langganan dan siriakannya dengan kemas kini bersyarat atau pembahagian yang teratur. cancel_at_period_end boleh wujud bersama dengan pembaharuan semasa; pembatalan serta-merta menyemak invois terbuka atau percubaan pembayaran sebelum mengeluarkan bayaran balik atau pembatalan (void). Peristiwa audit mesti menerangkan keputusan akhir pengebilan dan kelayakan.
Susulan 4: Bagaimanakah anda menambah pengebilan penggunaan tanpa pengiraan berganda?
Nyahduplikasi ID peristiwa penggunaan yang tidak boleh diubah dan cipta snapshot penggunaan bagi setiap tempoh pengebilan. Pendua meningkatkan metrik penerimaan tetapi bukan kuantiti yang boleh dibilkan. Bekukan snapshot semasa penyelesaian; salurkan peristiwa lewat ke tempoh seterusnya atau pelarasan manual. Simpan versi sumber dan pengagregatan dalam item baris invois supaya pengiraan semula boleh dibandingkan.