[{"data":1,"prerenderedAt":307},["ShallowReactive",2],{"tag-labels":3,"site-header":4,"article-jawaban-kunci-itu-milik-faktanya-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":183,"renderedTakeaway":184,"latestArticles":185,"toc":284},{"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":12,"tags":80,"thumbnail_image":21,"og_image":85,"series":86,"authors":174,"body_locale":177,"available_locales":178,"body":180,"takeway":181,"prerequisite":182},117,"Jawabannya: Kunci Itu Milik Faktanya","jawaban-kunci-itu-milik-faktanya","Kuncinya pindah dari pesan ke pembayaran, dan dua jalur itu butuh kebijakan yang berbeda waktu menemukan keanehan. Angka tiap variannya ada di sini.","2026-10-10T12:43:00.000Z",4,[81,82,83,84,70],"idempotency","reconciliation","webhook","postgres","\u002Fog\u002Fjawaban-kunci-itu-milik-faktanya.png",{"id":87,"title":69,"title_id":69,"slug":70,"description":88,"description_id":89,"articles":90,"children_series":173,"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,126,141,158],{"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":79,"hidden_from_listing":15,"tags":120,"thumbnail_image":21,"og_image":121,"series":122,"authors":123},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",[81,83,84,100,70],"\u002Fog\u002Fsaldo-naik-empat-kali-untuk-satu-pembayaran.png",{"id":87,"title":69,"title_id":69,"slug":70},[124],{"authors_id":125},{"id":108,"name":109,"slug":110,"role":111,"profile_picture":21},{"id":127,"title":128,"title_id":129,"slug":130,"excerpt":131,"excerpt_id":132,"has_translation":12,"published_at":133,"reading_time":134,"hidden_from_listing":15,"tags":135,"thumbnail_image":21,"og_image":136,"series":137,"authors":138},116,"The Webhook Arrived Late. The Reconciliation Job Had Already Run.","Webhook-nya Telat, Job Rekonsiliasinya Keburu Jalan","webhook-telat-job-rekonsiliasi-keburu-jalan","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.","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],"\u002Fog\u002Fwebhook-telat-job-rekonsiliasi-keburu-jalan.png",{"id":87,"title":69,"title_id":69,"slug":70},[139],{"authors_id":140},{"id":108,"name":109,"slug":110,"role":111,"profile_picture":21},{"id":142,"title":143,"title_id":144,"slug":145,"excerpt":146,"excerpt_id":147,"has_translation":12,"published_at":148,"reading_time":79,"hidden_from_listing":15,"tags":149,"thumbnail_image":21,"og_image":153,"series":154,"authors":155},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",[150,151,152,70],"python","scope","mutasi","\u002Fog\u002Ftiap-nota-isinya-pesanan-yang-sama.png",{"id":87,"title":69,"title_id":69,"slug":70},[156],{"authors_id":157},{"id":108,"name":109,"slug":110,"role":111,"profile_picture":21},{"id":159,"title":160,"title_id":161,"slug":162,"excerpt":163,"excerpt_id":164,"has_translation":12,"published_at":148,"reading_time":79,"hidden_from_listing":15,"tags":165,"thumbnail_image":21,"og_image":168,"series":169,"authors":170},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.",[150,166,167,70],"error-handling","validasi","\u002Fog\u002Fscript-yang-tidak-bilang-di-mana-salahnya.png",{"id":87,"title":69,"title_id":69,"slug":70},[171],{"authors_id":172},{"id":108,"name":109,"slug":110,"role":111,"profile_picture":21},[],[175],{"authors_id":176},{"id":108,"name":109,"slug":110,"role":111,"profile_picture":21},"id",[179,177],"en","\n## Hasil pengukuran\n\nSemua angka di sini dari run pengukuran, bukan perkiraan: satu database\nbersih untuk tiap varian. Pemeriksaannya 24.\n\n| varian | merah | catatan |\n|---|---|---|\n| kondisi awal | **14 dari 24** | kunci jalur webhook (`event_id`) dan kunci job (`recon:\u003Crun>:\u003Ccharge>`) nggak pernah bertemu |\n| klaim pindah ke `charge_id`, tapi job tetap punya tabel klaimnya sendiri | **13 dari 24** | bentuk gagalnya berubah: sekarang job-nya kena unique di ledger, jadi balasannya 500 |\n| nominal yang sudah tercatat ditimpa laporan | **3 dari 24** | barisnya tetap satu, tapi nominal yang benar sudah hilang tanpa jejak |\n| klaim per pembayaran di kedua jalur, dua kebijakan, tanpa kursor | **0 dari 24** | — |\n\nAngka konkret dari `make demo` di kondisi awal (enam pembayaran seharga 220.000): 12 baris untuk 6\npembayaran, saldo 665.000. Di branch `solusi`: satu baris per pembayaran, saldo tepat.\n\n## 1. Setiap bagian benar sendiri-sendiri. Itu masalahnya\n\nJalur webhook itu jawaban lab sebelumnya, dan tetap benar — untuk pertanyaan *\"apakah pesan ini\nsudah pernah saya terima?\"*\n\nJalur rekonsiliasi juga benar untuk pertanyaan yang dia jawab: *\"apakah run ini sudah memproses\nbaris laporan ini?\"* Karena id run selalu baru, jawabannya selalu \"belum\" — dan di situlah bugnya.\nJob yang dijalankan dua kali melaporkan `inserted=6` dua kali, dengan bangga.\n\nYang nggak pernah ada di sistem ini: satu tempat yang bisa menjawab **\"apakah pembayaran ini sudah\npernah saya catat?\"** Dua jalur, dua tabel, dua kebenaran.\n\n## 2. Kuncinya pindah dari pesan ke faktanya\n\n```\nsebelum:  processed_events(event_id PK)     satu pesan satu klaim\nsesudah:  payment_claims(charge_id PK)      satu pembayaran satu klaim\n```\n\n`charge_id` adalah kunci yang **sama** untuk kedua jalur, karena keduanya sedang membicarakan\npembayaran yang sama. `event_id` tetap disimpan — di ledger dan di baris klaimnya — tapi perannya\nberubah: dia jejak, bukan penentu.\n\nIkutannya di database: unique di `ledger_entries` pindah dari `event_id` ke `charge_id`, karena\nyang nggak boleh dua kali itu pembayarannya. Di produksi ini migrasi yang harus dikerjakan\nhati-hati: duplikat lamanya dibereskan dulu — dan itu keputusan bisnis, bukan keputusan SQL —\nbaru constraint-nya dipasang.\n\n## 3. Dua jalur, dua kebijakan\n\n| | klaim sudah ada, isinya sama | klaim sudah ada, nominal berbeda |\n|---|---|---|\n| **webhook** | balas jawaban pengiriman pertama (200) | **409** |\n| **job rekonsiliasi** | lewati (`skipped`) | **laporkan** di `mismatched`, jangan tulis apa pun |\n\nAlasannya beda penunggunya. Di ujung jalur webhook ada koneksi HTTP yang sedang menunggu jawaban:\ndia berhak dapat jawaban yang sama, dan kalau isinya bertentangan, 409 adalah jawaban yang benar.\nDi ujung jalur rekonsiliasi ada job yang sedang menyisir satu hari laporan: kalau satu baris jelek\nmembuat seluruh hari gagal, job-nya nggak akan pernah selesai — dan besok pagi baris yang sama akan\nmenggagalkannya lagi.\n\n## 4. Kursor itu gejala, bukan kebutuhan\n\nBegitu klaimnya per pembayaran, nggak ada lagi yang perlu dicatat tentang \"sudah sampai mana\".\nMenjalankan ulang satu hari penuh aman, dua run bersamaan aman (yang kalah klaim melewati baris\nitu), dan run yang cuma sempat dua baris bisa dilanjutkan oleh run berikutnya. Itu sebabnya tiga\npemeriksaan tentang run yang diulang, tumpang tindih, dan kepotong jadi hijau tanpa satu baris kode\ntambahan.\n\nKalimat \"kunci yang dimiliki faktanya, bukan pesannya\" itu saya baru ngerti setelah kena dua kali. Yang kedua lebih mahal, karena waktu itu saya sudah merasa sistemnya aman.\n\n## Jebakan yang sengaja dibiarkan\n\n**Memberi jalur kedua tabelnya sendiri.** Kelihatan paling rapi: kode jalur webhook nggak\ndisentuh, job punya tabelnya sendiri, nggak ada yang bertabrakan. Yang terjadi terukur: 13 dari 24\nmasih merah, dan bentuk gagalnya berubah — sekarang job-nya kena unique constraint di ledger, jadi\nbalasannya 500.\n\n**Menganggap laporan settlement sebagai kebenaran.** Varian \"yang tercatat ditimpa laporan\":\nbarisnya tetap satu, tapi nominal yang benar-benar terjadi sudah hilang tanpa jejak, dan saldonya\nsalah seribu. Kalau kamu memutuskan laporan memang lebih benar, keputusan itu boleh — tapi harus\ndisengaja, dan selisihnya tetap dicatat.\n\n**Menggagalkan job supaya \"kelihatan\".** Job yang melempar error di baris pertama laporan yang\njelek berhenti di situ selamanya, dan laporan hari itu nggak pernah selesai.\n\n## Pertanyaan buat kamu\n\n1. Job ini jalan tiap pagi untuk hari kemarin. Kalau gateway baru mengirim laporan hari itu tiga\n   hari kemudian, apa yang harus berubah — kode job-nya, atau jadwalnya?\n2. Kalau sebuah charge bisa punya dua fakta berbeda (refund setelah pembayaran), kunci apa yang\n   benar, dan kenapa `charge_id` sendirian nggak cukup?\n3. Webhook membalas 409 untuk nominal yang bertentangan, job melaporkannya. Apa yang lebih baik\n   untuk pengguna: baris penyesuaian otomatis, atau laporan harian yang harus dilihat manusia?\n4. Kalau tabel riwayat run dihapus sama sekali, apa yang hilang dari sistem ini? Jawab dulu sebelum\n   menghapusnya.\n","Klaimnya harus tentang pembayaran (`charge_id`), bukan tentang pesannya (`event_id`).\n- Satu jalur punya tabel klaimnya sendiri = dua kebenaran; yang harus disatukan adalah kuncinya, bukan alatnya.\n- Webhook membalas jawaban pengiriman pertama; job melewati yang cocok dan melaporkan yang berbeda, bukan menimpanya.\n- Menghapus id run dari kunci juga menghapus kebutuhan akan kursor: mengulang satu hari penuh jadi aman.",{"id":127,"title":128,"slug":130},"\u003Ch2 id=\"hasil-pengukuran\">Hasil pengukuran\u003C\u002Fh2>\n\u003Cp>Semua angka di sini dari run pengukuran, bukan perkiraan: satu database\nbersih untuk tiap varian. Pemeriksaannya 24.\u003C\u002Fp>\n\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>varian\u003C\u002Fth>\n\u003Cth>merah\u003C\u002Fth>\n\u003Cth>catatan\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\u003Ctr>\n\u003Ctd>kondisi awal\u003C\u002Ftd>\n\u003Ctd>\u003Cstrong>14 dari 24\u003C\u002Fstrong>\u003C\u002Ftd>\n\u003Ctd>kunci jalur webhook (\u003Ccode>event_id\u003C\u002Fcode>) dan kunci job (\u003Ccode>recon:&lt;run&gt;:&lt;charge&gt;\u003C\u002Fcode>) nggak pernah bertemu\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>klaim pindah ke \u003Ccode>charge_id\u003C\u002Fcode>, tapi job tetap punya tabel klaimnya sendiri\u003C\u002Ftd>\n\u003Ctd>\u003Cstrong>13 dari 24\u003C\u002Fstrong>\u003C\u002Ftd>\n\u003Ctd>bentuk gagalnya berubah: sekarang job-nya kena unique di ledger, jadi balasannya 500\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>nominal yang sudah tercatat ditimpa laporan\u003C\u002Ftd>\n\u003Ctd>\u003Cstrong>3 dari 24\u003C\u002Fstrong>\u003C\u002Ftd>\n\u003Ctd>barisnya tetap satu, tapi nominal yang benar sudah hilang tanpa jejak\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>klaim per pembayaran di kedua jalur, dua kebijakan, tanpa kursor\u003C\u002Ftd>\n\u003Ctd>\u003Cstrong>0 dari 24\u003C\u002Fstrong>\u003C\u002Ftd>\n\u003Ctd>—\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\u003C\u002Ftable>\n\u003Cp>Angka konkret dari \u003Ccode>make demo\u003C\u002Fcode> di kondisi awal (enam pembayaran seharga 220.000): 12 baris untuk 6\npembayaran, saldo 665.000. Di branch \u003Ccode>solusi\u003C\u002Fcode>: satu baris per pembayaran, saldo tepat.\u003C\u002Fp>\n\u003Ch2 id=\"1-setiap-bagian-benar-sendiri-sendiri-itu-masalahnya\">1. Setiap bagian benar sendiri-sendiri. Itu masalahnya\u003C\u002Fh2>\n\u003Cp>Jalur webhook itu jawaban lab sebelumnya, dan tetap benar — untuk pertanyaan \u003Cem>&quot;apakah pesan ini\nsudah pernah saya terima?&quot;\u003C\u002Fem>\u003C\u002Fp>\n\u003Cp>Jalur rekonsiliasi juga benar untuk pertanyaan yang dia jawab: \u003Cem>&quot;apakah run ini sudah memproses\nbaris laporan ini?&quot;\u003C\u002Fem> Karena id run selalu baru, jawabannya selalu &quot;belum&quot; — dan di situlah bugnya.\nJob yang dijalankan dua kali melaporkan \u003Ccode>inserted=6\u003C\u002Fcode> dua kali, dengan bangga.\u003C\u002Fp>\n\u003Cp>Yang nggak pernah ada di sistem ini: satu tempat yang bisa menjawab \u003Cstrong>&quot;apakah pembayaran ini sudah\npernah saya catat?&quot;\u003C\u002Fstrong> Dua jalur, dua tabel, dua kebenaran.\u003C\u002Fp>\n\u003Ch2 id=\"2-kuncinya-pindah-dari-pesan-ke-faktanya\">2. Kuncinya pindah dari pesan ke faktanya\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\">TEXT\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=\"sebelum%3A%20%20processed_events(event_id%20PK)%20%20%20%20%20satu%20pesan%20satu%20klaim%0Asesudah%3A%20%20payment_claims(charge_id%20PK)%20%20%20%20%20%20satu%20pembayaran%20satu%20klaim\" 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\">\u003Ccode>sebelum:  processed_events(event_id PK)     satu pesan satu klaim\nsesudah:  payment_claims(charge_id PK)      satu pembayaran satu klaim\u003C\u002Fcode>\u003C\u002Fpre>\n  \u003C\u002Fdiv>\n\u003C\u002Fdiv>\u003Cp>\u003Ccode>charge_id\u003C\u002Fcode> adalah kunci yang \u003Cstrong>sama\u003C\u002Fstrong> untuk kedua jalur, karena keduanya sedang membicarakan\npembayaran yang sama. \u003Ccode>event_id\u003C\u002Fcode> tetap disimpan — di ledger dan di baris klaimnya — tapi perannya\nberubah: dia jejak, bukan penentu.\u003C\u002Fp>\n\u003Cp>Ikutannya di database: unique di \u003Ccode>ledger_entries\u003C\u002Fcode> pindah dari \u003Ccode>event_id\u003C\u002Fcode> ke \u003Ccode>charge_id\u003C\u002Fcode>, karena\nyang nggak boleh dua kali itu pembayarannya. Di produksi ini migrasi yang harus dikerjakan\nhati-hati: duplikat lamanya dibereskan dulu — dan itu keputusan bisnis, bukan keputusan SQL —\nbaru constraint-nya dipasang.\u003C\u002Fp>\n\u003Ch2 id=\"3-dua-jalur-dua-kebijakan\">3. Dua jalur, dua kebijakan\u003C\u002Fh2>\n\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>\u003C\u002Fth>\n\u003Cth>klaim sudah ada, isinya sama\u003C\u002Fth>\n\u003Cth>klaim sudah ada, nominal berbeda\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\u003Ctr>\n\u003Ctd>\u003Cstrong>webhook\u003C\u002Fstrong>\u003C\u002Ftd>\n\u003Ctd>balas jawaban pengiriman pertama (200)\u003C\u002Ftd>\n\u003Ctd>\u003Cstrong>409\u003C\u002Fstrong>\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Cstrong>job rekonsiliasi\u003C\u002Fstrong>\u003C\u002Ftd>\n\u003Ctd>lewati (\u003Ccode>skipped\u003C\u002Fcode>)\u003C\u002Ftd>\n\u003Ctd>\u003Cstrong>laporkan\u003C\u002Fstrong> di \u003Ccode>mismatched\u003C\u002Fcode>, jangan tulis apa pun\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\u003C\u002Ftable>\n\u003Cp>Alasannya beda penunggunya. Di ujung jalur webhook ada koneksi HTTP yang sedang menunggu jawaban:\ndia berhak dapat jawaban yang sama, dan kalau isinya bertentangan, 409 adalah jawaban yang benar.\nDi ujung jalur rekonsiliasi ada job yang sedang menyisir satu hari laporan: kalau satu baris jelek\nmembuat seluruh hari gagal, job-nya nggak akan pernah selesai — dan besok pagi baris yang sama akan\nmenggagalkannya lagi.\u003C\u002Fp>\n\u003Ch2 id=\"4-kursor-itu-gejala-bukan-kebutuhan\">4. Kursor itu gejala, bukan kebutuhan\u003C\u002Fh2>\n\u003Cp>Begitu klaimnya per pembayaran, nggak ada lagi yang perlu dicatat tentang &quot;sudah sampai mana&quot;.\nMenjalankan ulang satu hari penuh aman, dua run bersamaan aman (yang kalah klaim melewati baris\nitu), dan run yang cuma sempat dua baris bisa dilanjutkan oleh run berikutnya. Itu sebabnya tiga\npemeriksaan tentang run yang diulang, tumpang tindih, dan kepotong jadi hijau tanpa satu baris kode\ntambahan.\u003C\u002Fp>\n\u003Cp>Kalimat &quot;kunci yang dimiliki faktanya, bukan pesannya&quot; itu saya baru ngerti setelah kena dua kali. Yang kedua lebih mahal, karena waktu itu saya sudah merasa sistemnya aman.\u003C\u002Fp>\n\u003Ch2 id=\"jebakan-yang-sengaja-dibiarkan\">Jebakan yang sengaja dibiarkan\u003C\u002Fh2>\n\u003Cp>\u003Cstrong>Memberi jalur kedua tabelnya sendiri.\u003C\u002Fstrong> Kelihatan paling rapi: kode jalur webhook nggak\ndisentuh, job punya tabelnya sendiri, nggak ada yang bertabrakan. Yang terjadi terukur: 13 dari 24\nmasih merah, dan bentuk gagalnya berubah — sekarang job-nya kena unique constraint di ledger, jadi\nbalasannya 500.\u003C\u002Fp>\n\u003Cp>\u003Cstrong>Menganggap laporan settlement sebagai kebenaran.\u003C\u002Fstrong> Varian &quot;yang tercatat ditimpa laporan&quot;:\nbarisnya tetap satu, tapi nominal yang benar-benar terjadi sudah hilang tanpa jejak, dan saldonya\nsalah seribu. Kalau kamu memutuskan laporan memang lebih benar, keputusan itu boleh — tapi harus\ndisengaja, dan selisihnya tetap dicatat.\u003C\u002Fp>\n\u003Cp>\u003Cstrong>Menggagalkan job supaya &quot;kelihatan&quot;.\u003C\u002Fstrong> Job yang melempar error di baris pertama laporan yang\njelek berhenti di situ selamanya, dan laporan hari itu nggak pernah selesai.\u003C\u002Fp>\n\u003Ch2 id=\"pertanyaan-buat-kamu\">Pertanyaan buat kamu\u003C\u002Fh2>\n\u003Col>\n\u003Cli>Job ini jalan tiap pagi untuk hari kemarin. Kalau gateway baru mengirim laporan hari itu tiga\nhari kemudian, apa yang harus berubah — kode job-nya, atau jadwalnya?\u003C\u002Fli>\n\u003Cli>Kalau sebuah charge bisa punya dua fakta berbeda (refund setelah pembayaran), kunci apa yang\nbenar, dan kenapa \u003Ccode>charge_id\u003C\u002Fcode> sendirian nggak cukup?\u003C\u002Fli>\n\u003Cli>Webhook membalas 409 untuk nominal yang bertentangan, job melaporkannya. Apa yang lebih baik\nuntuk pengguna: baris penyesuaian otomatis, atau laporan harian yang harus dilihat manusia?\u003C\u002Fli>\n\u003Cli>Kalau tabel riwayat run dihapus sama sekali, apa yang hilang dari sistem ini? Jawab dulu sebelum\nmenghapusnya.\u003C\u002Fli>\n\u003C\u002Fol>\n","\u003Cp>Klaimnya harus tentang pembayaran (\u003Ccode>charge_id\u003C\u002Fcode>), bukan tentang pesannya (\u003Ccode>event_id\u003C\u002Fcode>).\u003C\u002Fp>\n\u003Cul>\n\u003Cli>Satu jalur punya tabel klaimnya sendiri = dua kebenaran; yang harus disatukan adalah kuncinya, bukan alatnya.\u003C\u002Fli>\n\u003Cli>Webhook membalas jawaban pengiriman pertama; job melewati yang cocok dan melaporkan yang berbeda, bukan menimpanya.\u003C\u002Fli>\n\u003Cli>Menghapus id run dari kunci juga menghapus kebutuhan akan kursor: mengulang satu hari penuh jadi aman.\u003C\u002Fli>\n\u003C\u002Ful>\n",[186,204,217,230,243,264],{"id":187,"title":188,"title_id":21,"slug":189,"excerpt":190,"excerpt_id":21,"has_translation":15,"published_at":191,"reading_time":192,"hidden_from_listing":15,"tags":193,"thumbnail_image":21,"og_image":195,"series":196,"authors":201},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,[150,194],"string","\u002Fog\u002Fpython-strings.png",{"id":197,"title":198,"title_id":199,"slug":200},30,"Collections and Strings","Koleksi dan String","python-collections",[202],{"authors_id":203},{"id":108,"name":109,"slug":110,"role":111,"profile_picture":21},{"id":205,"title":206,"title_id":21,"slug":207,"excerpt":208,"excerpt_id":21,"has_translation":15,"published_at":209,"reading_time":192,"hidden_from_listing":15,"tags":210,"thumbnail_image":21,"og_image":212,"series":213,"authors":214},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",[150,211],"tuple","\u002Fog\u002Fpython-tuples.png",{"id":197,"title":198,"title_id":199,"slug":200},[215],{"authors_id":216},{"id":108,"name":109,"slug":110,"role":111,"profile_picture":21},{"id":218,"title":219,"title_id":21,"slug":220,"excerpt":221,"excerpt_id":21,"has_translation":15,"published_at":222,"reading_time":192,"hidden_from_listing":15,"tags":223,"thumbnail_image":21,"og_image":225,"series":226,"authors":227},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",[150,224],"set","\u002Fog\u002Fpython-sets.png",{"id":197,"title":198,"title_id":199,"slug":200},[228],{"authors_id":229},{"id":108,"name":109,"slug":110,"role":111,"profile_picture":21},{"id":231,"title":232,"title_id":21,"slug":233,"excerpt":234,"excerpt_id":21,"has_translation":15,"published_at":235,"reading_time":192,"hidden_from_listing":15,"tags":236,"thumbnail_image":21,"og_image":238,"series":239,"authors":240},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",[150,237],"dictionary","\u002Fog\u002Fpython-dictionary.png",{"id":197,"title":198,"title_id":199,"slug":200},[241],{"authors_id":242},{"id":108,"name":109,"slug":110,"role":111,"profile_picture":21},{"id":244,"title":245,"title_id":246,"slug":247,"excerpt":248,"excerpt_id":249,"has_translation":12,"published_at":250,"reading_time":14,"hidden_from_listing":15,"tags":251,"thumbnail_image":21,"og_image":258,"series":259,"authors":261},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",[252,253,254,255,256,257],"rag","cloud-run","cloud-tasks","gcp","serverless","arsitektur","\u002Fog\u002Frag-pisah-jalur-query-dan-ingestion.png",{"id":260,"title":35,"title_id":36,"slug":37},8,[262],{"authors_id":263},{"id":108,"name":109,"slug":110,"role":111,"profile_picture":21},{"id":265,"title":266,"title_id":267,"slug":268,"excerpt":269,"excerpt_id":270,"has_translation":12,"published_at":271,"reading_time":192,"hidden_from_listing":15,"tags":272,"thumbnail_image":21,"og_image":279,"series":280,"authors":281},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",[273,274,275,276,277,278],"multi-agent","langgraph","autogen","orkestrasi","checkpoint","llm","\u002Fog\u002Fmulti-agen-linear-atau-grafik.png",{"id":260,"title":35,"title_id":36,"slug":37},[282],{"authors_id":283},{"id":108,"name":109,"slug":110,"role":111,"profile_picture":21},[285,289,292,295,298,301,304],{"text":286,"depth":287,"id":288},"Hasil pengukuran",2,"hasil-pengukuran",{"text":290,"depth":287,"id":291},"1. Setiap bagian benar sendiri-sendiri. Itu masalahnya","1-setiap-bagian-benar-sendiri-sendiri-itu-masalahnya",{"text":293,"depth":287,"id":294},"2. Kuncinya pindah dari pesan ke faktanya","2-kuncinya-pindah-dari-pesan-ke-faktanya",{"text":296,"depth":287,"id":297},"3. Dua jalur, dua kebijakan","3-dua-jalur-dua-kebijakan",{"text":299,"depth":287,"id":300},"4. Kursor itu gejala, bukan kebutuhan","4-kursor-itu-gejala-bukan-kebutuhan",{"text":302,"depth":287,"id":303},"Jebakan yang sengaja dibiarkan","jebakan-yang-sengaja-dibiarkan",{"text":305,"depth":287,"id":306},"Pertanyaan buat kamu","pertanyaan-buat-kamu",1793336438882]