[{"data":1,"prerenderedAt":285},["ShallowReactive",2],{"tag-labels":3,"site-header":4,"article-saldo-naik-empat-kali-untuk-satu-pembayaran-id":69},{},{"featuredPost":5,"navigations":26},{"id":6,"title":7,"title_en":8,"slug":9,"excerpt":10,"excerpt_en":8,"has_translation":11,"published_at":12,"reading_time":13,"hidden_from_listing":11,"tags":14,"thumbnail_image":8,"og_image":19,"series":20,"authors":25},110,"Ketika Pilih Framework Cuma Biar Kelihatan Beda",null,"ketika-pilih-framework-cuma-biar-kelihatan-beda","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.",false,"2026-10-06T15:25:21.000Z",5,[15,16,17,18],"karier","framework","opini","pengalaman","\u002Fog\u002Fketika-pilih-framework-cuma-biar-kelihatan-beda.png",{"id":21,"title":22,"title_en":23,"slug":24},9,"Artikel","Articles","all",[],[27,43,64],{"collection":28,"item":29},"series",{"__typename":28,"title":22,"title_en":23,"slug":24,"children_series":30},[31,35,39],{"title":32,"title_en":33,"slug":34},"Kecerdasan Artifisial","Artificial Intelligence","kecerdasan-artifisial",{"title":36,"title_en":37,"slug":38},"Konsep Dasar Pemrograman","Programming Fundamentals","konsep-dasar-pemrograman",{"title":40,"title_en":41,"slug":42},"Algoritma dan Struktur Data","Algorithms and Data Structures","algoritma-struktur-data",{"collection":28,"item":44},{"__typename":28,"title":45,"title_en":45,"slug":46,"children_series":47},"Tutorial","tutorial",[48,52,56,60],{"title":49,"title_en":50,"slug":51},"Belajar Python","Learning Python","belajar-python",{"title":53,"title_en":54,"slug":55},"Belajar Golang","Learning Go","belajar-golang",{"title":57,"title_en":58,"slug":59},"Belajar Java","Learning Java","belajar-java",{"title":61,"title_en":62,"slug":63},"Belajar Pascal","Learning Pascal","belajar-pascal",{"collection":28,"item":65},{"__typename":28,"title":66,"title_en":66,"slug":67,"children_series":68},"Lab","lab",[],{"article":70,"renderedBody":173,"renderedTakeaway":174,"latestArticles":175,"toc":274},{"id":71,"title":72,"title_en":73,"slug":74,"excerpt":75,"excerpt_en":76,"has_translation":77,"published_at":78,"reading_time":79,"hidden_from_listing":11,"tags":80,"thumbnail_image":8,"og_image":85,"series":86,"authors":165,"body_locale":168,"available_locales":169,"body":171,"takeway":172,"prerequisite":8},114,"Saldo Naik Empat Kali untuk Satu Pembayaran","One Payment, Four Ledger Rows: An Idempotency Lab","saldo-naik-empat-kali-untuk-satu-pembayaran","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.","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.",true,"2026-10-11T13:45:00.000Z",4,[81,82,83,84,67],"idempotency","webhook","postgres","database","\u002Fog\u002Fsaldo-naik-empat-kali-untuk-satu-pembayaran.png",{"id":87,"title":66,"title_en":66,"slug":67,"description":88,"description_en":89,"articles":90,"children_series":164,"parent_series":8},29,"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.","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.",[91,111,116,132,149],{"id":92,"title":93,"title_en":94,"slug":95,"excerpt":96,"excerpt_en":97,"has_translation":77,"published_at":98,"reading_time":13,"hidden_from_listing":11,"tags":99,"thumbnail_image":8,"og_image":102,"series":103,"authors":104},104,"Query-nya Tinggal 2, Tapi Response-nya Masih 1 Detik","N+1 Was Never the Bottleneck: 41 Queries Down to 2, Still One Second","query-tinggal-2-tapi-response-masih-1-detik","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.","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.","2026-10-04T09:59:21.000Z",[84,100,101],"performance","backend","\u002Fog\u002Fquery-tinggal-2-tapi-response-masih-1-detik.png",{"id":87,"title":66,"title_en":66,"slug":67},[105],{"authors_id":106},{"id":107,"name":108,"slug":109,"role":110,"profile_picture":8},1,"Daffa Izzuddin","izzudd","Writer",{"id":71,"title":72,"title_en":73,"slug":74,"excerpt":75,"excerpt_en":76,"has_translation":77,"published_at":78,"reading_time":79,"hidden_from_listing":11,"tags":80,"thumbnail_image":8,"og_image":85,"series":112,"authors":113},{"id":87,"title":66,"title_en":66,"slug":67},[114],{"authors_id":115},{"id":107,"name":108,"slug":109,"role":110,"profile_picture":8},{"id":117,"title":118,"title_en":119,"slug":120,"excerpt":121,"excerpt_en":122,"has_translation":77,"published_at":123,"reading_time":124,"hidden_from_listing":11,"tags":125,"thumbnail_image":8,"og_image":127,"series":128,"authors":129},116,"Webhook-nya Telat, Job Rekonsiliasinya Keburu Jalan","The Webhook Arrived Late. The Reconciliation Job Had Already Run.","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.","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.","2026-10-11T13:44:00.000Z",3,[81,126,82,83,67],"reconciliation","\u002Fog\u002Fwebhook-telat-job-rekonsiliasi-keburu-jalan.png",{"id":87,"title":66,"title_en":66,"slug":67},[130],{"authors_id":131},{"id":107,"name":108,"slug":109,"role":110,"profile_picture":8},{"id":133,"title":134,"title_en":135,"slug":136,"excerpt":137,"excerpt_en":138,"has_translation":77,"published_at":139,"reading_time":79,"hidden_from_listing":11,"tags":140,"thumbnail_image":8,"og_image":144,"series":145,"authors":146},120,"Tiap Nota di Kafe Ini Isinya Pesanan yang Sama","Every Receipt in This Café Lists the Same Order","tiap-nota-isinya-pesanan-yang-sama","Nota tiap meja isinya sama semua, dan totalnya Rp0 — padahal tiap meja cuma pesan satu barang. Coba tebak kenapa.","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.","2026-10-11T23:00:15.000Z",[141,142,143,67],"python","scope","mutasi","\u002Fog\u002Ftiap-nota-isinya-pesanan-yang-sama.png",{"id":87,"title":66,"title_en":66,"slug":67},[147],{"authors_id":148},{"id":107,"name":108,"slug":109,"role":110,"profile_picture":8},{"id":150,"title":151,"title_en":152,"slug":153,"excerpt":154,"excerpt_en":155,"has_translation":77,"published_at":139,"reading_time":79,"hidden_from_listing":11,"tags":156,"thumbnail_image":8,"og_image":159,"series":160,"authors":161},122,"Script yang Nggak Bisa Bilang di Mana Salahnya","A Script That Cannot Say Where It Went Wrong","script-yang-tidak-bilang-di-mana-salahnya","Script yang berhenti di baris jelek pertama cuma melaporkan sebagian masalah yang ada. Ini yang muncul di layar sekarang.","A script that stops at the first bad line reports only part of the problem. This is what shows up on screen right now.",[141,157,158,67],"error-handling","validasi","\u002Fog\u002Fscript-yang-tidak-bilang-di-mana-salahnya.png",{"id":87,"title":66,"title_en":66,"slug":67},[162],{"authors_id":163},{"id":107,"name":108,"slug":109,"role":110,"profile_picture":8},[],[166],{"authors_id":167},{"id":107,"name":108,"slug":109,"role":110,"profile_picture":8},"id",[170,168],"en","Sebuah merchant komplain: saldonya bertambah, tapi bukan dengan jumlah yang benar. Di laporan\nmutasi, satu pembayaran senilai dua puluh lima ribu muncul **empat kali**. Nilai saldonya naik\nempat kali lipat dari yang seharusnya.\n\nKamu buka kodenya. Endpoint `POST \u002Fwebhooks\u002Fpayments` — satu request masuk, satu baris ledger\nkeluar, saldo ditambah. Nggak ada `try\u002Fexcept` yang mencurigakan, nggak ada perulangan,\nnggak ada bug. Kalau kamu tes dengan mengirim request yang sama dua kali, hasilnya ya dua baris —\ntapi kamu juga yang mengirimnya dua kali, jadi itu wajar.\n\nSaya pernah ketemu sepupunya masalah ini di kerjaan pertama saya: bukan saldo yang dobel, tapi stok gudang yang angkanya ketinggalan beberapa langkah, dan barangnya keburu kebeli orang sebelum angkanya membaik. Bentuknya beda, akarnya sama — dan itu sebabnya lab ini ada. [Ceritanya di sini](\u002Fp\u002Fpub-sub-sendiri-lalu-belajar-idempotency).\n\nYang nggak kelihatan: **HTTP nggak punya cara memberi tahu pengirim bahwa jawaban kita sampai.**\nGateway mengirim eventnya, kita commit transaksinya, lalu jawabannya hilang di jalan — koneksi\nputus, load balancer timeout, container kita di-restart sedetik terlalu cepat. Dari sisi kita\nsemuanya beres. Dari sisi gateway, eventnya belum pernah diterima, jadi dia kirim lagi.\n\nItu bukan gateway yang rusak. Itu satu-satunya pilihan yang dia punya, dan dia memilih **dobel\ndaripada hilang** — karena kehilangan pembayaran lebih mahal daripada mengulangnya. Dobelnya jadi\npekerjaanmu.\n\nIni yang terjadi kalau kamu jalankan lab-nya:\n\n| | kondisi awal |\n|---|---|\n| satu event dikirim ulang 4× | **4 baris** ledger untuk 1 event, saldo naik **4× nominal** |\n| satu event dikirim 30× sekaligus | **30 baris**, dan saldo cuma naik sebagian (40.000–60.000 dari 300.000 yang seharusnya) |\n| 3 event berbeda, masing-masing dikirim 2× | **6 baris**, saldo naik **6× nominal** |\n| request yang persis sama dikirim 2× | **2 baris** — nggak ada apa pun di database yang bisa membedakan mana yang sudah pernah diproses |\n\nBaris kedua itu layak dibaca ulang. Dua puluh baris masuk ke ledger, tapi saldonya cuma naik dua\nkali — artinya ada **dua** masalah berbeda di satu endpoint, dan yang kedua bukan soal idempotensi\nsama sekali.\n\nKejadian yang sama bisa sampai ke sistemmu lewat tiga cara, dan ketiganya pernah saya alami:\n\n1. **Datang lebih dari sekali.** Gateway mengirim ulang karena jawabannya nggak sampai. Lab ini soal yang ini.\n2. **Datang dari dua jalur.** Webhook yang telat, plus job rekonsiliasi yang keburu menutup hari itu. Sudutnya ada di [lab satunya](\u002Fp\u002Fwebhook-telat-job-rekonsiliasi-keburu-jalan).\n3. **Datang tidak berurutan.** Update yang datang belakangan membawa data yang lebih tua, dan yang lebih tua menang. Ini yang saya alami di stok gudang, dan bentuknya beda dari dua yang di atas.\n\nAkarnya satu: keputusan \"kejadian ini sudah pernah diterapkan atau belum\" diambil berdasarkan **kedatangan pesannya**, di aplikasi. Yang berhak memutuskan bukan aplikasimu, dan yang jadi patokan bukan urutan kedatangan.\n\n## Misi lab-nya\n\nEmpat syarat, dan `make check` memeriksa keempatnya:\n\n1. **Tepat sekali.** Satu event diterapkan tepat satu kali. Tidak dobel, dan tidak ada yang\n   hilang: tiga event berbeda tetap jadi tiga baris ledger, bukan enam, bukan dua.\n2. **Pengiriman ulang dijawab seperti jawaban pertama.** Bukan error, bukan pesan \"sudah\n   diproses\" — body yang sama, dengan `entry_id` yang sama. Alasannya bukan sekadar sopan:\n   jawaban yang dihitung ulang dari keadaan database **saat itu** bisa salah, karena saldonya\n   sudah berubah oleh event lain.\n3. **`event_id` yang dipakai ulang dengan isi berbeda tidak ikut diproses.** Nominal yang\n   berbeda untuk event yang sama tidak boleh masuk ke ledger.\n4. **Saldo tetap benar saat banyak event datang bersamaan.** Tiga puluh event *berbeda* yang\n   datang sekaligus harus menaikkan saldo tiga puluh kali nominal, bukan dua kali.\n\n## Kalau kamu mau mencobanya\n\nLab-nya repo publik, semuanya jalan di dalam Docker — nggak perlu install Postgres, Python, atau\napa pun:\n\n```bash\ngit clone https:\u002F\u002Fgithub.com\u002Fizzudd\u002Finva-lab.git\ncd inva-lab\u002F0002.idempotency\nmake up        # database + API\nmake replay    # putar ulang satu event, seperti yang dilakukan gateway\nmake check     # target: 21 pemeriksaan, semua hijau\n```\n\nAda `HINTS.md` dengan tiga tingkat petunjuk, dan aturan mainnya satu: jangan buka branch `solusi`\nsebelum kamu benar-benar mentok.\n\n## Pertanyaannya\n\nSebelum kamu menambahkan pengecekan apa pun, jawab ini dulu: **kenapa gateway mengirim ulang,\npadahal permintaan pertamanya dibalas 200?**\n\nKalau jawabannya belum jelas, semua tambalan yang kamu tulis cuma menutupi gejala. Dan begitu\njawabannya jelas, muncul pertanyaan kedua yang lebih menarik: kalau pengiriman ulang itu normal\ndan bukan kesalahan siapa pun, **siapa yang seharusnya memutuskan bahwa pembayaran ini sudah\npernah diterima — aplikasimu, atau database-nya?**\n\nVersi panjangnya, lengkap dengan argumen kenapa menambah `UNIQUE` saja justru bikin masalah baru,\nada di [artikel jawabannya](\u002Fp\u002Fjawaban-konflik-bukan-error).\n","Empat baris ledger untuk satu pembayaran, dan nggak ada bug di kodenya.\n- HTTP nggak bisa memberi tahu pengirim bahwa jawabannya sampai, jadi pengiriman ulang itu normal, bukan kesalahan siapa pun.\n- Gateway memilih dobel daripada hilang; dobelnya yang jadi pekerjaanmu.\n- Dua puluh baris masuk ke ledger sementara saldo cuma naik dua kali: ada dua masalah berbeda di satu endpoint.\n- Pertanyaannya bukan \"di mana bugnya\", tapi siapa yang berhak memutuskan bahwa sebuah pembayaran sudah pernah diterima.","\u003Cp>Sebuah merchant komplain: saldonya bertambah, tapi bukan dengan jumlah yang benar. Di laporan\nmutasi, satu pembayaran senilai dua puluh lima ribu muncul \u003Cstrong>empat kali\u003C\u002Fstrong>. Nilai saldonya naik\nempat kali lipat dari yang seharusnya.\u003C\u002Fp>\n\u003Cp>Kamu buka kodenya. Endpoint \u003Ccode>POST \u002Fwebhooks\u002Fpayments\u003C\u002Fcode> — satu request masuk, satu baris ledger\nkeluar, saldo ditambah. Nggak ada \u003Ccode>try\u002Fexcept\u003C\u002Fcode> yang mencurigakan, nggak ada perulangan,\nnggak ada bug. Kalau kamu tes dengan mengirim request yang sama dua kali, hasilnya ya dua baris —\ntapi kamu juga yang mengirimnya dua kali, jadi itu wajar.\u003C\u002Fp>\n\u003Cp>Saya pernah ketemu sepupunya masalah ini di kerjaan pertama saya: bukan saldo yang dobel, tapi stok gudang yang angkanya ketinggalan beberapa langkah, dan barangnya keburu kebeli orang sebelum angkanya membaik. Bentuknya beda, akarnya sama — dan itu sebabnya lab ini ada. \u003Ca href=\"\u002Fp\u002Fpub-sub-sendiri-lalu-belajar-idempotency\">Ceritanya di sini\u003C\u002Fa>.\u003C\u002Fp>\n\u003Cp>Yang nggak kelihatan: \u003Cstrong>HTTP nggak punya cara memberi tahu pengirim bahwa jawaban kita sampai.\u003C\u002Fstrong>\nGateway mengirim eventnya, kita commit transaksinya, lalu jawabannya hilang di jalan — koneksi\nputus, load balancer timeout, container kita di-restart sedetik terlalu cepat. Dari sisi kita\nsemuanya beres. Dari sisi gateway, eventnya belum pernah diterima, jadi dia kirim lagi.\u003C\u002Fp>\n\u003Cp>Itu bukan gateway yang rusak. Itu satu-satunya pilihan yang dia punya, dan dia memilih \u003Cstrong>dobel\ndaripada hilang\u003C\u002Fstrong> — karena kehilangan pembayaran lebih mahal daripada mengulangnya. Dobelnya jadi\npekerjaanmu.\u003C\u002Fp>\n\u003Cp>Ini yang terjadi kalau kamu jalankan lab-nya:\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>satu event dikirim ulang 4×\u003C\u002Ftd>\n\u003Ctd>\u003Cstrong>4 baris\u003C\u002Fstrong> ledger untuk 1 event, saldo naik \u003Cstrong>4× nominal\u003C\u002Fstrong>\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>satu event dikirim 30× sekaligus\u003C\u002Ftd>\n\u003Ctd>\u003Cstrong>30 baris\u003C\u002Fstrong>, dan saldo cuma naik sebagian (40.000–60.000 dari 300.000 yang seharusnya)\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>3 event berbeda, masing-masing dikirim 2×\u003C\u002Ftd>\n\u003Ctd>\u003Cstrong>6 baris\u003C\u002Fstrong>, saldo naik \u003Cstrong>6× nominal\u003C\u002Fstrong>\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>request yang persis sama dikirim 2×\u003C\u002Ftd>\n\u003Ctd>\u003Cstrong>2 baris\u003C\u002Fstrong> — nggak ada apa pun di database yang bisa membedakan mana yang sudah pernah diproses\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\u003C\u002Ftable>\n\u003Cp>Baris kedua itu layak dibaca ulang. Dua puluh baris masuk ke ledger, tapi saldonya cuma naik dua\nkali — artinya ada \u003Cstrong>dua\u003C\u002Fstrong> masalah berbeda di satu endpoint, dan yang kedua bukan soal idempotensi\nsama sekali.\u003C\u002Fp>\n\u003Cp>Kejadian yang sama bisa sampai ke sistemmu lewat tiga cara, dan ketiganya pernah saya alami:\u003C\u002Fp>\n\u003Col>\n\u003Cli>\u003Cstrong>Datang lebih dari sekali.\u003C\u002Fstrong> Gateway mengirim ulang karena jawabannya nggak sampai. Lab ini soal yang ini.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Datang dari dua jalur.\u003C\u002Fstrong> Webhook yang telat, plus job rekonsiliasi yang keburu menutup hari itu. Sudutnya ada di \u003Ca href=\"\u002Fp\u002Fwebhook-telat-job-rekonsiliasi-keburu-jalan\">lab satunya\u003C\u002Fa>.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Datang tidak berurutan.\u003C\u002Fstrong> Update yang datang belakangan membawa data yang lebih tua, dan yang lebih tua menang. Ini yang saya alami di stok gudang, dan bentuknya beda dari dua yang di atas.\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Cp>Akarnya satu: keputusan &quot;kejadian ini sudah pernah diterapkan atau belum&quot; diambil berdasarkan \u003Cstrong>kedatangan pesannya\u003C\u002Fstrong>, di aplikasi. Yang berhak memutuskan bukan aplikasimu, dan yang jadi patokan bukan urutan kedatangan.\u003C\u002Fp>\n\u003Ch2 id=\"misi-lab-nya\">Misi lab-nya\u003C\u002Fh2>\n\u003Cp>Empat syarat, dan \u003Ccode>make check\u003C\u002Fcode> memeriksa keempatnya:\u003C\u002Fp>\n\u003Col>\n\u003Cli>\u003Cstrong>Tepat sekali.\u003C\u002Fstrong> Satu event diterapkan tepat satu kali. Tidak dobel, dan tidak ada yang\nhilang: tiga event berbeda tetap jadi tiga baris ledger, bukan enam, bukan dua.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Pengiriman ulang dijawab seperti jawaban pertama.\u003C\u002Fstrong> Bukan error, bukan pesan &quot;sudah\ndiproses&quot; — body yang sama, dengan \u003Ccode>entry_id\u003C\u002Fcode> yang sama. Alasannya bukan sekadar sopan:\njawaban yang dihitung ulang dari keadaan database \u003Cstrong>saat itu\u003C\u002Fstrong> bisa salah, karena saldonya\nsudah berubah oleh event lain.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>\u003Ccode>event_id\u003C\u002Fcode> yang dipakai ulang dengan isi berbeda tidak ikut diproses.\u003C\u002Fstrong> Nominal yang\nberbeda untuk event yang sama tidak boleh masuk ke ledger.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Saldo tetap benar saat banyak event datang bersamaan.\u003C\u002Fstrong> Tiga puluh event \u003Cem>berbeda\u003C\u002Fem> yang\ndatang sekaligus harus menaikkan saldo tiga puluh kali nominal, bukan dua kali.\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Ch2 id=\"kalau-kamu-mau-mencobanya\">Kalau kamu mau mencobanya\u003C\u002Fh2>\n\u003Cp>Lab-nya repo publik, semuanya jalan di dalam Docker — nggak perlu install Postgres, Python, atau\napa pun:\u003C\u002Fp>\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%2F0002.idempotency%0Amake%20up%20%20%20%20%20%20%20%20%23%20database%20%2B%20API%0Amake%20replay%20%20%20%20%23%20putar%20ulang%20satu%20event%2C%20seperti%20yang%20dilakukan%20gateway%0Amake%20check%20%20%20%20%20%23%20target%3A%2021%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\u002F0002.idempotency\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\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">make\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\"> replay\u003C\u002Fspan>\u003Cspan style=\"color:#6A737D\">    # putar ulang satu event, seperti yang dilakukan gateway\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: 21 pemeriksaan, semua hijau\u003C\u002Fspan>\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n  \u003C\u002Fdiv>\n\u003C\u002Fdiv>\u003Cp>Ada \u003Ccode>HINTS.md\u003C\u002Fcode> dengan tiga tingkat petunjuk, dan aturan mainnya satu: jangan buka branch \u003Ccode>solusi\u003C\u002Fcode>\nsebelum kamu benar-benar mentok.\u003C\u002Fp>\n\u003Ch2 id=\"pertanyaannya\">Pertanyaannya\u003C\u002Fh2>\n\u003Cp>Sebelum kamu menambahkan pengecekan apa pun, jawab ini dulu: \u003Cstrong>kenapa gateway mengirim ulang,\npadahal permintaan pertamanya dibalas 200?\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Cp>Kalau jawabannya belum jelas, semua tambalan yang kamu tulis cuma menutupi gejala. Dan begitu\njawabannya jelas, muncul pertanyaan kedua yang lebih menarik: kalau pengiriman ulang itu normal\ndan bukan kesalahan siapa pun, \u003Cstrong>siapa yang seharusnya memutuskan bahwa pembayaran ini sudah\npernah diterima — aplikasimu, atau database-nya?\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Cp>Versi panjangnya, lengkap dengan argumen kenapa menambah \u003Ccode>UNIQUE\u003C\u002Fcode> saja justru bikin masalah baru,\nada di \u003Ca href=\"\u002Fp\u002Fjawaban-konflik-bukan-error\">artikel jawabannya\u003C\u002Fa>.\u003C\u002Fp>\n","\u003Cp>Empat baris ledger untuk satu pembayaran, dan nggak ada bug di kodenya.\u003C\u002Fp>\n\u003Cul>\n\u003Cli>HTTP nggak bisa memberi tahu pengirim bahwa jawabannya sampai, jadi pengiriman ulang itu normal, bukan kesalahan siapa pun.\u003C\u002Fli>\n\u003Cli>Gateway memilih dobel daripada hilang; dobelnya yang jadi pekerjaanmu.\u003C\u002Fli>\n\u003Cli>Dua puluh baris masuk ke ledger sementara saldo cuma naik dua kali: ada dua masalah berbeda di satu endpoint.\u003C\u002Fli>\n\u003Cli>Pertanyaannya bukan &quot;di mana bugnya&quot;, tapi siapa yang berhak memutuskan bahwa sebuah pembayaran sudah pernah diterima.\u003C\u002Fli>\n\u003C\u002Ful>\n",[176,194,207,220,239,257],{"id":177,"title":178,"title_en":8,"slug":179,"excerpt":180,"excerpt_en":8,"has_translation":11,"published_at":181,"reading_time":182,"hidden_from_listing":11,"tags":183,"thumbnail_image":8,"og_image":185,"series":186,"authors":191},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",6,[141,184],"tuple","\u002Fog\u002Fpython-tuples.png",{"id":187,"title":188,"title_en":189,"slug":190},30,"Koleksi dan String","Collections and Strings","python-collections",[192],{"authors_id":193},{"id":107,"name":108,"slug":109,"role":110,"profile_picture":8},{"id":195,"title":196,"title_en":8,"slug":197,"excerpt":198,"excerpt_en":8,"has_translation":11,"published_at":199,"reading_time":182,"hidden_from_listing":11,"tags":200,"thumbnail_image":8,"og_image":202,"series":203,"authors":204},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",[141,201],"set","\u002Fog\u002Fpython-sets.png",{"id":187,"title":188,"title_en":189,"slug":190},[205],{"authors_id":206},{"id":107,"name":108,"slug":109,"role":110,"profile_picture":8},{"id":208,"title":209,"title_en":8,"slug":210,"excerpt":211,"excerpt_en":8,"has_translation":11,"published_at":212,"reading_time":182,"hidden_from_listing":11,"tags":213,"thumbnail_image":8,"og_image":215,"series":216,"authors":217},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",[141,214],"dictionary","\u002Fog\u002Fpython-dictionary.png",{"id":187,"title":188,"title_en":189,"slug":190},[218],{"authors_id":219},{"id":107,"name":108,"slug":109,"role":110,"profile_picture":8},{"id":221,"title":222,"title_en":8,"slug":223,"excerpt":224,"excerpt_en":8,"has_translation":11,"published_at":225,"reading_time":13,"hidden_from_listing":11,"tags":226,"thumbnail_image":8,"og_image":233,"series":234,"authors":236},129,"Biar RAG nggak kena 504: pisahin jalur query dan ingestion","rag-pisah-jalur-query-dan-ingestion","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",[227,228,229,230,231,232],"rag","cloud-run","cloud-tasks","gcp","serverless","arsitektur","\u002Fog\u002Frag-pisah-jalur-query-dan-ingestion.png",{"id":235,"title":32,"title_en":33,"slug":34},8,[237],{"authors_id":238},{"id":107,"name":108,"slug":109,"role":110,"profile_picture":8},{"id":240,"title":241,"title_en":8,"slug":242,"excerpt":243,"excerpt_en":8,"has_translation":11,"published_at":244,"reading_time":182,"hidden_from_listing":11,"tags":245,"thumbnail_image":8,"og_image":252,"series":253,"authors":254},128,"Linear atau graf: cara mengatur banyak agent tanpa loop abadi","multi-agen-linear-atau-grafik","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",[246,247,248,249,250,251],"multi-agent","langgraph","autogen","orkestrasi","checkpoint","llm","\u002Fog\u002Fmulti-agen-linear-atau-grafik.png",{"id":235,"title":32,"title_en":33,"slug":34},[255],{"authors_id":256},{"id":107,"name":108,"slug":109,"role":110,"profile_picture":8},{"id":258,"title":259,"title_en":8,"slug":260,"excerpt":261,"excerpt_en":8,"has_translation":11,"published_at":262,"reading_time":182,"hidden_from_listing":11,"tags":263,"thumbnail_image":8,"og_image":269,"series":270,"authors":271},127,"Kenapa RAG yang cuma andalkan cosine similarity sering nyasar","kenapa-rag-cuma-cosine-similarity-sering-nyasar","RAG yang cuma andalkan embedding balikin chunk yang mirip maknanya, bukan yang kamu butuh. Ini penyebabnya, dan dua lapis penambalnya.","2026-10-19T02:04:06.718Z",[227,264,265,266,267,268],"vector-search","bm25","hybrid-search","retrieval","embedding","\u002Fog\u002Fkenapa-rag-cuma-cosine-similarity-sering-nyasar.png",{"id":235,"title":32,"title_en":33,"slug":34},[272],{"authors_id":273},{"id":107,"name":108,"slug":109,"role":110,"profile_picture":8},[275,279,282],{"text":276,"depth":277,"id":278},"Misi lab-nya",2,"misi-lab-nya",{"text":280,"depth":277,"id":281},"Kalau kamu mau mencobanya","kalau-kamu-mau-mencobanya",{"text":283,"depth":277,"id":284},"Pertanyaannya","pertanyaannya",1793246467078]