[{"data":1,"prerenderedAt":290},["ShallowReactive",2],{"tag-labels":3,"site-header":4,"article-webhook-telat-job-rekonsiliasi-keburu-jalan-id":72},{},{"featuredPost":5,"navigations":29},{"id":6,"title":7,"title_id":8,"slug":9,"excerpt":10,"excerpt_id":11,"has_translation":12,"published_at":13,"reading_time":14,"hidden_from_listing":15,"tags":16,"thumbnail_image":21,"og_image":22,"series":23,"authors":28},110,"When You Pick a Framework Just to Look Different","Ketika Pilih Framework Cuma Biar Kelihatan Beda","ketika-pilih-framework-cuma-biar-kelihatan-beda","The last time you chose a new framework or library, was it because your problem needed that tool, or because it sounded cool and few people around you were using it? That question is uncomfortable. Technology decisions are often made for reasons that never appear in any technical document.","Terakhir kali kamu milih framework atau library baru, itu karena masalahmu butuh alat itu, atau karena alatnya kedengaran keren dan belum banyak yang pakai? Pertanyaan ini nggak enak dijawab. Soalnya keputusan teknologi sering diambil dengan alasan yang nggak pernah ditulis di dokumen teknis mana pun.",true,"2026-10-06T15:25:21.000Z",5,false,[17,18,19,20],"karier","framework","opini","pengalaman",null,"\u002Fog\u002Fketika-pilih-framework-cuma-biar-kelihatan-beda.png",{"id":24,"title":25,"title_id":26,"slug":27},9,"Articles","Artikel","all",[],[30,46,67],{"collection":31,"item":32},"series",{"__typename":31,"title":25,"title_id":26,"slug":27,"children_series":33},[34,38,42],{"title":35,"title_id":36,"slug":37},"Artificial Intelligence","Kecerdasan Artifisial","kecerdasan-artifisial",{"title":39,"title_id":40,"slug":41},"Programming Fundamentals","Konsep Dasar Pemrograman","konsep-dasar-pemrograman",{"title":43,"title_id":44,"slug":45},"Algorithms and Data Structures","Algoritma dan Struktur Data","algoritma-struktur-data",{"collection":31,"item":47},{"__typename":31,"title":48,"title_id":48,"slug":49,"children_series":50},"Tutorial","tutorial",[51,55,59,63],{"title":52,"title_id":53,"slug":54},"Learning Python","Belajar Python","belajar-python",{"title":56,"title_id":57,"slug":58},"Learning Go","Belajar Golang","belajar-golang",{"title":60,"title_id":61,"slug":62},"Learning Java","Belajar Java","belajar-java",{"title":64,"title_id":65,"slug":66},"Learning Pascal","Belajar Pascal","belajar-pascal",{"collection":31,"item":68},{"__typename":31,"title":69,"title_id":69,"slug":70,"children_series":71},"Lab","lab",[],{"article":73,"renderedBody":175,"renderedTakeaway":176,"latestArticles":177,"toc":276},{"id":74,"title":75,"title_id":75,"slug":76,"excerpt":77,"excerpt_id":77,"has_translation":12,"published_at":78,"reading_time":79,"hidden_from_listing":15,"tags":80,"thumbnail_image":21,"og_image":85,"series":86,"authors":167,"body_locale":170,"available_locales":171,"body":173,"takeway":174,"prerequisite":21},116,"Webhook-nya Telat, Job Rekonsiliasinya Keburu Jalan","webhook-telat-job-rekonsiliasi-keburu-jalan","Jalur webhook sudah idempotent. Lalu ada job rekonsiliasi pagi, dan satu pembayaran tercatat dua kali dari dua jalur yang dua-duanya benar. Coba tebak kuncinya salah di mana.","2026-10-11T13:44:00.000Z",3,[81,82,83,84,70],"idempotency","reconciliation","webhook","postgres","\u002Fog\u002Fwebhook-telat-job-rekonsiliasi-keburu-jalan.png",{"id":87,"title":69,"title_id":69,"slug":70,"description":88,"description_id":89,"articles":90,"children_series":166,"parent_series":21},29,"Practice labs: work the problem first, then read the answer. Every lab ships with a repo you can clone, production-sized data, and a target you have to hit yourself. The write-up lives in this series too, and it only opens from the problem article.","Lab praktik: kerjakan masalahnya dulu, baru lihat jawabannya. Tiap lab punya repo yang bisa di-clone, data seukuran produksi, dan target yang harus kamu capai sendiri. Artikel jawabannya ada di seri ini juga, dan cuma bisa kamu buka dari artikel masalahnya.",[91,112,127,134,151],{"id":92,"title":93,"title_id":94,"slug":95,"excerpt":96,"excerpt_id":97,"has_translation":12,"published_at":98,"reading_time":14,"hidden_from_listing":15,"tags":99,"thumbnail_image":21,"og_image":103,"series":104,"authors":105},104,"N+1 Was Never the Bottleneck: 41 Queries Down to 2, Still One Second","Query-nya Tinggal 2, Tapi Response-nya Masih 1 Detik","query-tinggal-2-tapi-response-masih-1-detik","A feed endpoint that shows 20 posts takes a full second and hits the database with 41 queries. Apply the standard N+1 advice, get it down to 2 queries, and the latency does not move. This is that problem as it happened, plus the lab.","Endpoint feed yang cuma menampilkan 20 tulisan butuh satu detik, dan server database dihujani 41 query. Kamu kerjakan nasihat N+1 yang standar, query-nya turun jadi 2, dan latency-nya nggak bergerak. Ini versi \"apa adanya\" dari masalah itu, plus lab-nya.","2026-10-04T09:59:21.000Z",[100,101,102],"database","performance","backend","\u002Fog\u002Fquery-tinggal-2-tapi-response-masih-1-detik.png",{"id":87,"title":69,"title_id":69,"slug":70},[106],{"authors_id":107},{"id":108,"name":109,"slug":110,"role":111,"profile_picture":21},1,"Daffa Izzuddin","izzudd","Writer",{"id":113,"title":114,"title_id":115,"slug":116,"excerpt":117,"excerpt_id":118,"has_translation":12,"published_at":119,"reading_time":120,"hidden_from_listing":15,"tags":121,"thumbnail_image":21,"og_image":122,"series":123,"authors":124},114,"One Payment, Four Ledger Rows: An Idempotency Lab","Saldo Naik Empat Kali untuk Satu Pembayaran","saldo-naik-empat-kali-untuk-satu-pembayaran","The code is not wrong and the gateway is not wrong, yet one payment shows up four times in the ledger. Guess why, then make `make check` green in the lab.","Kodenya nggak salah, dan gateway-nya juga nggak salah. Tapi satu pembayaran muncul empat kali di ledger. Coba tebak kenapa — lalu bikin `make check` hijau di lab-nya.","2026-10-11T13:45:00.000Z",4,[81,83,84,100,70],"\u002Fog\u002Fsaldo-naik-empat-kali-untuk-satu-pembayaran.png",{"id":87,"title":69,"title_id":69,"slug":70},[125],{"authors_id":126},{"id":108,"name":109,"slug":110,"role":111,"profile_picture":21},{"id":74,"title":128,"title_id":75,"slug":76,"excerpt":129,"excerpt_id":77,"has_translation":12,"published_at":78,"reading_time":79,"hidden_from_listing":15,"tags":80,"thumbnail_image":21,"og_image":85,"series":130,"authors":131},"The Webhook Arrived Late. The Reconciliation Job Had Already Run.","The webhook path is already idempotent. Then a morning reconciliation job appears, and one payment gets recorded twice from two paths that are both right. Guess where the key is wrong.",{"id":87,"title":69,"title_id":69,"slug":70},[132],{"authors_id":133},{"id":108,"name":109,"slug":110,"role":111,"profile_picture":21},{"id":135,"title":136,"title_id":137,"slug":138,"excerpt":139,"excerpt_id":140,"has_translation":12,"published_at":141,"reading_time":120,"hidden_from_listing":15,"tags":142,"thumbnail_image":21,"og_image":146,"series":147,"authors":148},120,"Every Receipt in This Café Lists the Same Order","Tiap Nota di Kafe Ini Isinya Pesanan yang Sama","tiap-nota-isinya-pesanan-yang-sama","Every table's receipt holds the same items, and the total is Rp0 — even though each table ordered exactly one thing. Guess why, then make `make check` green. A Python scope and mutation lab.","Nota tiap meja isinya sama semua, dan totalnya Rp0 — padahal tiap meja cuma pesan satu barang. Coba tebak kenapa.","2026-10-11T23:00:15.000Z",[143,144,145,70],"python","scope","mutasi","\u002Fog\u002Ftiap-nota-isinya-pesanan-yang-sama.png",{"id":87,"title":69,"title_id":69,"slug":70},[149],{"authors_id":150},{"id":108,"name":109,"slug":110,"role":111,"profile_picture":21},{"id":152,"title":153,"title_id":154,"slug":155,"excerpt":156,"excerpt_id":157,"has_translation":12,"published_at":141,"reading_time":120,"hidden_from_listing":15,"tags":158,"thumbnail_image":21,"og_image":161,"series":162,"authors":163},122,"A Script That Cannot Say Where It Went Wrong","Script yang Nggak Bisa Bilang di Mana Salahnya","script-yang-tidak-bilang-di-mana-salahnya","A script that stops at the first bad line reports only part of the problem. This is what shows up on screen right now.","Script yang berhenti di baris jelek pertama cuma melaporkan sebagian masalah yang ada. Ini yang muncul di layar sekarang.",[143,159,160,70],"error-handling","validasi","\u002Fog\u002Fscript-yang-tidak-bilang-di-mana-salahnya.png",{"id":87,"title":69,"title_id":69,"slug":70},[164],{"authors_id":165},{"id":108,"name":109,"slug":110,"role":111,"profile_picture":21},[],[168],{"authors_id":169},{"id":108,"name":109,"slug":110,"role":111,"profile_picture":21},"id",[172,170],"en","\nGateway-nya down semalam. Pembayaran yang terjadi di sisi mereka nggak ada yang sampai ke kita\nlewat webhook, dan pagi harinya job rekonsiliasi membaca laporan settlement hari itu lalu menutup\nyang belum tercatat. Sampai di sini semua benar — job-nya bekerja persis seperti yang dimaksud.\n\nLalu jam sebelas siang, gateway mengirim ulang webhook yang belum pernah dijawab. Dan satu\npembayaran yang sama tercatat dua kali: satu dari job, satu dari webhook.\n\nBukan karena salah satunya rusak. Jalur webhook-nya sudah idempotent — dia jawaban lab sebelumnya,\nlengkap dengan klaim per event dan jawaban yang disimpan. Job-nya juga benar untuk pertanyaan yang\ndia jawab. Masalahnya cuma satu: **keduanya memakai kunci yang berbeda, dan nggak ada satu pun dari\nkeduanya yang tahu bahwa jalur satunya ada.**\n\n| | kondisi awal |\n|---|---|\n| 3 webhook malam itu, lalu job rekonsiliasi paginya | 6 pembayaran jadi **9 baris** — 3 di antaranya punya dua baris, satu dari tiap jalur |\n| webhook yang telat akhirnya sampai | 6 pembayaran jadi **12 baris**; saldo 665.000 dari 220.000 yang seharusnya |\n| job rekonsiliasi dijalankan dua kali | **6 baris baru** tiap kali, dan job-nya melaporkan `inserted=6` — bukan 0 |\n| dua run job jalan bersamaan | **12 baris** untuk 6 pembayaran |\n| laporan settlement beda nominal dengan yang sudah tercatat | **2 baris** untuk 1 pembayaran, dan `mismatched` tetap kosong |\n\nPerhatikan baris ketiga. Job-nya bukan cuma menulis dobel — dia **melaporkan** bahwa dia menulis\nenam baris baru, dengan yakin, untuk hari yang sebenarnya sudah selesai.\n\nDi kerjaan pertama saya jalur keduanya bukan job rekonsiliasi, tapi panggilan balik ke backend storefront yang beda — dan bentuk masalahnya persis sama: satu fakta, dua jalur, dua kunci yang nggak pernah bertemu. [Ceritanya di sini](\u002Fp\u002Fpub-sub-sendiri-lalu-belajar-idempotency).\n\nIni sudut kedua dari tiga sudut yang saya rapikan di situ:\n\n1. **Datang lebih dari sekali** → [lab sebelumnya](\u002Fp\u002Fsaldo-naik-empat-kali-untuk-satu-pembayaran).\n2. **Datang dari dua jalur** → lab ini.\n3. **Datang tidak berurutan** → yang bikin stok gudang saya salah; bentuknya beda dari lab ini.\n\nAkarnya satu: keputusan \"sudah pernah diterapkan atau belum\" diambil berdasarkan kedatangan\npesannya, di aplikasi — bukan di database, dengan kunci dan patokan waktu yang dimiliki faktanya\nsendiri.\n\n## Misi lab-nya\n\nTiga syarat:\n\n1. **Satu pembayaran, satu baris.** Dari jalur mana pun, dan berapa kali pun jalurnya dipanggil.\n2. **Job boleh dijalankan lagi, dan laporannya jujur.** Run kedua untuk hari yang sama nggak\n   menambah baris dan melaporkan `inserted=0`. Run yang cuma sanggup memproses sebagian boleh\n   dilanjutkan kapan saja. Dua run yang tumpang tindih juga nggak boleh mendobel.\n3. **Nominal yang sudah tercatat nggak berubah diam-diam.** Kalau laporan settlement menyebut\n   nominal yang berbeda, barisnya tetap satu, nominalnya tetap yang pertama, dan perbedaannya\n   **dilaporkan** — bukan ditelan.\n\n## Kenapa ini layak dicoba\n\nDi lab sebelumnya, \"pembayaran\" dan \"pesan yang memberitahukan pembayaran itu\" selalu datang\nberpasangan, jadi nggak pernah ada bedanya. Begitu ada jalur kedua, bedanya jadi hal paling penting\ndi seluruh sistem: **kunci yang benar bukan kunci yang unik, tapi kunci yang dimiliki oleh\nfaktanya — bukan oleh pesannya.**\n\nDan bagian kedua yang lebih halus: dua jalur yang sama-sama benar bisa butuh **kebijakan yang\nberbeda** waktu menemukan keanehan. Webhook boleh membalas 409 untuk satu event yang aneh; job\nrekonsiliasi nggak boleh mati cuma karena satu baris laporan nggak cocok.\n\n## Kalau kamu mau mencobanya\n\n```bash\ngit clone https:\u002F\u002Fgithub.com\u002Fizzudd\u002Finva-lab.git\ncd inva-lab\u002F0003.reconciliation\nmake up      # database + API + gateway tiruan\nmake demo    # cerita lengkapnya: webhook malam itu, job paginya, webhook yang telat\nmake check   # target: 24 pemeriksaan, semua hijau\n```\n\nGateway-nya tiruan dan laporannya dibangkitkan dari tanggalnya, jadi kamu bisa pakai tanggal apa\npun: `make demo DAY=2024-03-17`.\n\n## Pertanyaannya\n\nJob rekonsiliasi itu jalur kedua yang menerima satu fakta lewat dua kunci berbeda. Sebelum\nmenyentuh kode, jawab ini: **kunci mana yang berasal dari pembayarannya, dan kunci mana yang\nberasal dari pesan yang memberitahukannya?** Dan yang kedua: kenapa job-nya harus membuat kuncinya\nsendiri, padahal jalur webhook sudah punya kunci yang unik?\n\nJawaban lengkapnya, termasuk kenapa menghapus id run dari kuncinya juga menghapus kebutuhan akan\nkursor, ada di [artikel jawabannya](\u002Fp\u002Fjawaban-kunci-itu-milik-faktanya).\n","Dua jalur yang sama-sama benar tetap bisa mencatat satu pembayaran dua kali.\n- Jalur webhook memakai kunci milik pesannya; job rekonsiliasi membuat kuncinya sendiri — dan keduanya nggak pernah bertemu.\n- Job yang dijalankan dua kali melaporkan `inserted=6` dua kali, dengan yakin, untuk hari yang sudah selesai.\n- Kunci yang benar bukan kunci yang unik, tapi kunci yang dimiliki oleh faktanya, bukan oleh pesannya.\n- Dua jalur boleh punya kebijakan berbeda waktu menemukan keanehan — penunggunya memang berbeda.","\u003Cp>Gateway-nya down semalam. Pembayaran yang terjadi di sisi mereka nggak ada yang sampai ke kita\nlewat webhook, dan pagi harinya job rekonsiliasi membaca laporan settlement hari itu lalu menutup\nyang belum tercatat. Sampai di sini semua benar — job-nya bekerja persis seperti yang dimaksud.\u003C\u002Fp>\n\u003Cp>Lalu jam sebelas siang, gateway mengirim ulang webhook yang belum pernah dijawab. Dan satu\npembayaran yang sama tercatat dua kali: satu dari job, satu dari webhook.\u003C\u002Fp>\n\u003Cp>Bukan karena salah satunya rusak. Jalur webhook-nya sudah idempotent — dia jawaban lab sebelumnya,\nlengkap dengan klaim per event dan jawaban yang disimpan. Job-nya juga benar untuk pertanyaan yang\ndia jawab. Masalahnya cuma satu: \u003Cstrong>keduanya memakai kunci yang berbeda, dan nggak ada satu pun dari\nkeduanya yang tahu bahwa jalur satunya ada.\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>\u003C\u002Fth>\n\u003Cth>kondisi awal\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\u003Ctr>\n\u003Ctd>3 webhook malam itu, lalu job rekonsiliasi paginya\u003C\u002Ftd>\n\u003Ctd>6 pembayaran jadi \u003Cstrong>9 baris\u003C\u002Fstrong> — 3 di antaranya punya dua baris, satu dari tiap jalur\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>webhook yang telat akhirnya sampai\u003C\u002Ftd>\n\u003Ctd>6 pembayaran jadi \u003Cstrong>12 baris\u003C\u002Fstrong>; saldo 665.000 dari 220.000 yang seharusnya\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>job rekonsiliasi dijalankan dua kali\u003C\u002Ftd>\n\u003Ctd>\u003Cstrong>6 baris baru\u003C\u002Fstrong> tiap kali, dan job-nya melaporkan \u003Ccode>inserted=6\u003C\u002Fcode> — bukan 0\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>dua run job jalan bersamaan\u003C\u002Ftd>\n\u003Ctd>\u003Cstrong>12 baris\u003C\u002Fstrong> untuk 6 pembayaran\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>laporan settlement beda nominal dengan yang sudah tercatat\u003C\u002Ftd>\n\u003Ctd>\u003Cstrong>2 baris\u003C\u002Fstrong> untuk 1 pembayaran, dan \u003Ccode>mismatched\u003C\u002Fcode> tetap kosong\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\u003C\u002Ftable>\n\u003Cp>Perhatikan baris ketiga. Job-nya bukan cuma menulis dobel — dia \u003Cstrong>melaporkan\u003C\u002Fstrong> bahwa dia menulis\nenam baris baru, dengan yakin, untuk hari yang sebenarnya sudah selesai.\u003C\u002Fp>\n\u003Cp>Di kerjaan pertama saya jalur keduanya bukan job rekonsiliasi, tapi panggilan balik ke backend storefront yang beda — dan bentuk masalahnya persis sama: satu fakta, dua jalur, dua kunci yang nggak pernah bertemu. \u003Ca href=\"\u002Fp\u002Fpub-sub-sendiri-lalu-belajar-idempotency\">Ceritanya di sini\u003C\u002Fa>.\u003C\u002Fp>\n\u003Cp>Ini sudut kedua dari tiga sudut yang saya rapikan di situ:\u003C\u002Fp>\n\u003Col>\n\u003Cli>\u003Cstrong>Datang lebih dari sekali\u003C\u002Fstrong> → \u003Ca href=\"\u002Fp\u002Fsaldo-naik-empat-kali-untuk-satu-pembayaran\">lab sebelumnya\u003C\u002Fa>.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Datang dari dua jalur\u003C\u002Fstrong> → lab ini.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Datang tidak berurutan\u003C\u002Fstrong> → yang bikin stok gudang saya salah; bentuknya beda dari lab ini.\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Cp>Akarnya satu: keputusan &quot;sudah pernah diterapkan atau belum&quot; diambil berdasarkan kedatangan\npesannya, di aplikasi — bukan di database, dengan kunci dan patokan waktu yang dimiliki faktanya\nsendiri.\u003C\u002Fp>\n\u003Ch2 id=\"misi-lab-nya\">Misi lab-nya\u003C\u002Fh2>\n\u003Cp>Tiga syarat:\u003C\u002Fp>\n\u003Col>\n\u003Cli>\u003Cstrong>Satu pembayaran, satu baris.\u003C\u002Fstrong> Dari jalur mana pun, dan berapa kali pun jalurnya dipanggil.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Job boleh dijalankan lagi, dan laporannya jujur.\u003C\u002Fstrong> Run kedua untuk hari yang sama nggak\nmenambah baris dan melaporkan \u003Ccode>inserted=0\u003C\u002Fcode>. Run yang cuma sanggup memproses sebagian boleh\ndilanjutkan kapan saja. Dua run yang tumpang tindih juga nggak boleh mendobel.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Nominal yang sudah tercatat nggak berubah diam-diam.\u003C\u002Fstrong> Kalau laporan settlement menyebut\nnominal yang berbeda, barisnya tetap satu, nominalnya tetap yang pertama, dan perbedaannya\n\u003Cstrong>dilaporkan\u003C\u002Fstrong> — bukan ditelan.\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Ch2 id=\"kenapa-ini-layak-dicoba\">Kenapa ini layak dicoba\u003C\u002Fh2>\n\u003Cp>Di lab sebelumnya, &quot;pembayaran&quot; dan &quot;pesan yang memberitahukan pembayaran itu&quot; selalu datang\nberpasangan, jadi nggak pernah ada bedanya. Begitu ada jalur kedua, bedanya jadi hal paling penting\ndi seluruh sistem: \u003Cstrong>kunci yang benar bukan kunci yang unik, tapi kunci yang dimiliki oleh\nfaktanya — bukan oleh pesannya.\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Cp>Dan bagian kedua yang lebih halus: dua jalur yang sama-sama benar bisa butuh \u003Cstrong>kebijakan yang\nberbeda\u003C\u002Fstrong> waktu menemukan keanehan. Webhook boleh membalas 409 untuk satu event yang aneh; job\nrekonsiliasi nggak boleh mati cuma karena satu baris laporan nggak cocok.\u003C\u002Fp>\n\u003Ch2 id=\"kalau-kamu-mau-mencobanya\">Kalau kamu mau mencobanya\u003C\u002Fh2>\n\n\u003Cdiv class=\"code-block-wrapper my-6 not-prose rounded-xl border-2 border-black dark:border-gray-600 shadow-neo overflow-hidden bg-[#24292e] text-[#e1e4e8] transition-all\">\n  \u003Cdiv class=\"code-block-header flex items-center justify-between px-4 py-2.5 bg-[#1f2428] border-b-2 border-black dark:border-gray-600 font-mono text-xs font-bold text-gray-300 select-none\">\n    \u003Cdiv class=\"flex items-center gap-2\">\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#ff5f56] border border-black\u002F40 inline-block\">\u003C\u002Fspan>\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#ffbd2e] border border-black\u002F40 inline-block\">\u003C\u002Fspan>\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#27c93f] border border-black\u002F40 inline-block\">\u003C\u002Fspan>\n      \u003Cspan class=\"ml-2 font-mono font-bold text-xs uppercase tracking-wider text-gray-300\">BASH\u003C\u002Fspan>\n    \u003C\u002Fdiv>\n    \u003Cbutton type=\"button\" class=\"copy-code-btn flex items-center gap-1.5 px-3 py-1 text-xs font-bold bg-[#2f363d] text-gray-200 border-2 border-black dark:border-gray-500 rounded-lg shadow-neo-sm hover:translate-x-0.5 hover:translate-y-0.5 hover:shadow-none hover:bg-gray-700 transition-all cursor-pointer\" data-code=\"git%20clone%20https%3A%2F%2Fgithub.com%2Fizzudd%2Finva-lab.git%0Acd%20inva-lab%2F0003.reconciliation%0Amake%20up%20%20%20%20%20%20%23%20database%20%2B%20API%20%2B%20gateway%20tiruan%0Amake%20demo%20%20%20%20%23%20cerita%20lengkapnya%3A%20webhook%20malam%20itu%2C%20job%20paginya%2C%20webhook%20yang%20telat%0Amake%20check%20%20%20%23%20target%3A%2024%20pemeriksaan%2C%20semua%20hijau\" title=\"Salin kode\">\n      \u003Csvg class=\"w-3.5 h-3.5 copy-icon-svg\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n        \u003Cpath stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z\">\u003C\u002Fpath>\n      \u003C\u002Fsvg>\n      \u003Cspan class=\"copy-text\">Copy\u003C\u002Fspan>\n    \u003C\u002Fbutton>\n  \u003C\u002Fdiv>\n  \u003Cdiv class=\"code-block-content p-4 overflow-x-auto text-sm font-mono leading-relaxed bg-[#24292e]\">\n    \u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">git\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\"> clone\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\"> https:\u002F\u002Fgithub.com\u002Fizzudd\u002Finva-lab.git\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">cd\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\"> inva-lab\u002F0003.reconciliation\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">make\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\"> up\u003C\u002Fspan>\u003Cspan style=\"color:#6A737D\">      # database + API + gateway tiruan\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">make\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\"> demo\u003C\u002Fspan>\u003Cspan style=\"color:#6A737D\">    # cerita lengkapnya: webhook malam itu, job paginya, webhook yang telat\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">make\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\"> check\u003C\u002Fspan>\u003Cspan style=\"color:#6A737D\">   # target: 24 pemeriksaan, semua hijau\u003C\u002Fspan>\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n  \u003C\u002Fdiv>\n\u003C\u002Fdiv>\u003Cp>Gateway-nya tiruan dan laporannya dibangkitkan dari tanggalnya, jadi kamu bisa pakai tanggal apa\npun: \u003Ccode>make demo DAY=2024-03-17\u003C\u002Fcode>.\u003C\u002Fp>\n\u003Ch2 id=\"pertanyaannya\">Pertanyaannya\u003C\u002Fh2>\n\u003Cp>Job rekonsiliasi itu jalur kedua yang menerima satu fakta lewat dua kunci berbeda. Sebelum\nmenyentuh kode, jawab ini: \u003Cstrong>kunci mana yang berasal dari pembayarannya, dan kunci mana yang\nberasal dari pesan yang memberitahukannya?\u003C\u002Fstrong> Dan yang kedua: kenapa job-nya harus membuat kuncinya\nsendiri, padahal jalur webhook sudah punya kunci yang unik?\u003C\u002Fp>\n\u003Cp>Jawaban lengkapnya, termasuk kenapa menghapus id run dari kuncinya juga menghapus kebutuhan akan\nkursor, ada di \u003Ca href=\"\u002Fp\u002Fjawaban-kunci-itu-milik-faktanya\">artikel jawabannya\u003C\u002Fa>.\u003C\u002Fp>\n","\u003Cp>Dua jalur yang sama-sama benar tetap bisa mencatat satu pembayaran dua kali.\u003C\u002Fp>\n\u003Cul>\n\u003Cli>Jalur webhook memakai kunci milik pesannya; job rekonsiliasi membuat kuncinya sendiri — dan keduanya nggak pernah bertemu.\u003C\u002Fli>\n\u003Cli>Job yang dijalankan dua kali melaporkan \u003Ccode>inserted=6\u003C\u002Fcode> dua kali, dengan yakin, untuk hari yang sudah selesai.\u003C\u002Fli>\n\u003Cli>Kunci yang benar bukan kunci yang unik, tapi kunci yang dimiliki oleh faktanya, bukan oleh pesannya.\u003C\u002Fli>\n\u003Cli>Dua jalur boleh punya kebijakan berbeda waktu menemukan keanehan — penunggunya memang berbeda.\u003C\u002Fli>\n\u003C\u002Ful>\n",[178,196,209,222,235,256],{"id":179,"title":180,"title_id":21,"slug":181,"excerpt":182,"excerpt_id":21,"has_translation":15,"published_at":183,"reading_time":184,"hidden_from_listing":15,"tags":185,"thumbnail_image":21,"og_image":187,"series":188,"authors":193},133,"Python Strings: the Badge That Only Worked for One Name","python-strings","The badge script printed the right name for the person who wrote it and the wrong name for everybody else. Slicing counts characters, and a name is words.","2026-10-28T13:00:00.000Z",6,[143,186],"string","\u002Fog\u002Fpython-strings.png",{"id":189,"title":190,"title_id":191,"slug":192},30,"Collections and Strings","Koleksi dan String","python-collections",[194],{"authors_id":195},{"id":108,"name":109,"slug":110,"role":111,"profile_picture":21},{"id":197,"title":198,"title_id":21,"slug":199,"excerpt":200,"excerpt_id":21,"has_translation":15,"published_at":201,"reading_time":184,"hidden_from_listing":15,"tags":202,"thumbnail_image":21,"og_image":204,"series":205,"authors":206},132,"Python Tuples: Why All Three Points Are the Same Point","python-tuples","Three points walk into a loop and come out identical. The list was never copied three times; the name was. A tuple makes that bug impossible.","2026-10-28T12:00:00.000Z",[143,203],"tuple","\u002Fog\u002Fpython-tuples.png",{"id":189,"title":190,"title_id":191,"slug":192},[207],{"authors_id":208},{"id":108,"name":109,"slug":110,"role":111,"profile_picture":21},{"id":210,"title":211,"title_id":21,"slug":212,"excerpt":213,"excerpt_id":21,"has_translation":15,"published_at":214,"reading_time":184,"hidden_from_listing":15,"tags":215,"thumbnail_image":21,"og_image":217,"series":218,"authors":219},131,"Python Sets: the Duplicate That Keeps Coming Back","python-sets","A de-duplication loop that works on clean data leaves four tags where there should be two. The bug is case, and the fix is a `set`.","2026-10-28T11:00:00.000Z",[143,216],"set","\u002Fog\u002Fpython-sets.png",{"id":189,"title":190,"title_id":191,"slug":192},[220],{"authors_id":221},{"id":108,"name":109,"slug":110,"role":111,"profile_picture":21},{"id":223,"title":224,"title_id":21,"slug":225,"excerpt":226,"excerpt_id":21,"has_translation":15,"published_at":227,"reading_time":184,"hidden_from_listing":15,"tags":228,"thumbnail_image":21,"og_image":230,"series":231,"authors":232},130,"Python Dictionaries: the Key That Isn't There","python-dictionary","A word counter dies the first time it meets a word twice. The fix is one line; what takes longer is knowing which of a dict's three lookups to reach for.","2026-10-28T10:00:00.000Z",[143,229],"dictionary","\u002Fog\u002Fpython-dictionary.png",{"id":189,"title":190,"title_id":191,"slug":192},[233],{"authors_id":234},{"id":108,"name":109,"slug":110,"role":111,"profile_picture":21},{"id":236,"title":237,"title_id":238,"slug":239,"excerpt":240,"excerpt_id":241,"has_translation":12,"published_at":242,"reading_time":14,"hidden_from_listing":15,"tags":243,"thumbnail_image":21,"og_image":250,"series":251,"authors":253},129,"So Your RAG Stops Hitting 504: Split the Query Path From Ingestion","Biar RAG nggak kena 504: pisahin jalur query dan ingestion","rag-pisah-jalur-query-dan-ingestion","One RAG service that both parses uploads and answers queries will fall over the moment someone uploads a 200-page PDF. The cure is two paths and one queue.","Satu service RAG yang merangkap parsing dan menjawab kueri bakal tumbang pas ada yang upload PDF 200 halaman. Obatnya dua jalur dan satu queue.","2026-10-22T01:30:00.000Z",[244,245,246,247,248,249],"rag","cloud-run","cloud-tasks","gcp","serverless","arsitektur","\u002Fog\u002Frag-pisah-jalur-query-dan-ingestion.png",{"id":252,"title":35,"title_id":36,"slug":37},8,[254],{"authors_id":255},{"id":108,"name":109,"slug":110,"role":111,"profile_picture":21},{"id":257,"title":258,"title_id":259,"slug":260,"excerpt":261,"excerpt_id":262,"has_translation":12,"published_at":263,"reading_time":184,"hidden_from_listing":15,"tags":264,"thumbnail_image":21,"og_image":271,"series":272,"authors":273},128,"Linear or Graph: How to Run Many Agents Without an Eternal Loop","Linear atau graf: cara mengatur banyak agent tanpa loop abadi","multi-agen-linear-atau-grafik","One agent with twenty tools gets messy fast. Once several agents revise each other, you need a graph that allows cycles and a number that makes it stop.","Satu agent dengan dua puluh tool gampang kacau. Begitu beberapa agent saling revisi, kamu butuh graf yang mendukung cycle dan angka yang membuatnya berhenti.","2026-10-21T16:05:00.000Z",[265,266,267,268,269,270],"multi-agent","langgraph","autogen","orkestrasi","checkpoint","llm","\u002Fog\u002Fmulti-agen-linear-atau-grafik.png",{"id":252,"title":35,"title_id":36,"slug":37},[274],{"authors_id":275},{"id":108,"name":109,"slug":110,"role":111,"profile_picture":21},[277,281,284,287],{"text":278,"depth":279,"id":280},"Misi lab-nya",2,"misi-lab-nya",{"text":282,"depth":279,"id":283},"Kenapa ini layak dicoba","kenapa-ini-layak-dicoba",{"text":285,"depth":279,"id":286},"Kalau kamu mau mencobanya","kalau-kamu-mau-mencobanya",{"text":288,"depth":279,"id":289},"Pertanyaannya","pertanyaannya",1793336438843]