[{"data":1,"prerenderedAt":307},["ShallowReactive",2],{"tag-labels":3,"site-header":4,"article-jawaban-konflik-bukan-error-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":183,"renderedTakeaway":184,"latestArticles":185,"toc":284},{"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":77,"tags":80,"thumbnail_image":8,"og_image":85,"series":86,"authors":174,"body_locale":177,"available_locales":178,"body":180,"takeway":181,"prerequisite":182},115,"Jawabannya: Konflik Bukan Error","The answer: A Conflict Is Not an Error","jawaban-konflik-bukan-error","Menambah unique key membuat error duplicate key di mana-mana, dan itu bukan tanda solusinya salah. Ini urutan perbaikannya, lengkap dengan angka hasil pengukuran tiap varian.","Adding a unique key produces duplicate-key errors everywhere, and that is not a sign the fix is wrong. Here is the order of repairs, with measured numbers for every variant.",true,"2026-10-10T12:43:00.000Z",4,[81,82,83,84,67],"idempotency","webhook","postgres","database","\u002Fog\u002Fjawaban-konflik-bukan-error.png",{"id":87,"title":66,"title_en":66,"slug":67,"description":88,"description_en":89,"articles":90,"children_series":173,"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,125,141,158],{"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":112,"title":113,"title_en":114,"slug":115,"excerpt":116,"excerpt_en":117,"has_translation":77,"published_at":118,"reading_time":79,"hidden_from_listing":11,"tags":119,"thumbnail_image":8,"og_image":120,"series":121,"authors":122},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.","2026-10-11T13:45:00.000Z",[81,82,83,84,67],"\u002Fog\u002Fsaldo-naik-empat-kali-untuk-satu-pembayaran.png",{"id":87,"title":66,"title_en":66,"slug":67},[123],{"authors_id":124},{"id":107,"name":108,"slug":109,"role":110,"profile_picture":8},{"id":126,"title":127,"title_en":128,"slug":129,"excerpt":130,"excerpt_en":131,"has_translation":77,"published_at":132,"reading_time":133,"hidden_from_listing":11,"tags":134,"thumbnail_image":8,"og_image":136,"series":137,"authors":138},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,135,82,83,67],"reconciliation","\u002Fog\u002Fwebhook-telat-job-rekonsiliasi-keburu-jalan.png",{"id":87,"title":66,"title_en":66,"slug":67},[139],{"authors_id":140},{"id":107,"name":108,"slug":109,"role":110,"profile_picture":8},{"id":142,"title":143,"title_en":144,"slug":145,"excerpt":146,"excerpt_en":147,"has_translation":77,"published_at":148,"reading_time":79,"hidden_from_listing":11,"tags":149,"thumbnail_image":8,"og_image":153,"series":154,"authors":155},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",[150,151,152,67],"python","scope","mutasi","\u002Fog\u002Ftiap-nota-isinya-pesanan-yang-sama.png",{"id":87,"title":66,"title_en":66,"slug":67},[156],{"authors_id":157},{"id":107,"name":108,"slug":109,"role":110,"profile_picture":8},{"id":159,"title":160,"title_en":161,"slug":162,"excerpt":163,"excerpt_en":164,"has_translation":77,"published_at":148,"reading_time":79,"hidden_from_listing":11,"tags":165,"thumbnail_image":8,"og_image":168,"series":169,"authors":170},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.",[150,166,167,67],"error-handling","validasi","\u002Fog\u002Fscript-yang-tidak-bilang-di-mana-salahnya.png",{"id":87,"title":66,"title_en":66,"slug":67},[171],{"authors_id":172},{"id":107,"name":108,"slug":109,"role":110,"profile_picture":8},[],[175],{"authors_id":176},{"id":107,"name":108,"slug":109,"role":110,"profile_picture":8},"id",[179,177],"en","Yang paling menarik dari lab ini bukan jawabannya, tapi **urutan gagalnya**. Karena urutan itulah\nkenapa banyak orang menyimpulkan \"unique key nggak menyelesaikan apa-apa\" — padahal constraint-nya\nbenar, dan database-nya benar.\n\n## Hasil pengukuran\n\nSemua angka di sini dari run pengukuran, bukan perkiraan: satu database\nbersih untuk tiap varian. Pemeriksaannya 21. Angka di kolom terakhir: berapa yang masih merah.\n\n| varian | merah | yang masih merah |\n|---|---|---|\n| kondisi awal | **11 dari 21** | dobel, saldo, jawaban, dan `event_id` yang dipakai ulang |\n| (1) + `UNIQUE (event_id)` di database, handler tidak diubah | **7 dari 21** | dijawab 2xx, jawaban, saldo |\n| (2) + klaim `ON CONFLICT DO NOTHING`, tapi konflik dijawab pesan | **1 dari 21** | body pengiriman ulang beda |\n| (3) + jawaban pengiriman pertama disimpan di baris klaimnya | **1 dari 21** | saldo |\n| (4) + saldo dihitung database, bukan dibaca-ubah-tulis di Python | **0 dari 21** | — |\n| alternatif untuk (2): periksa dulu di aplikasi, baru `INSERT` | **3 dari 21** | 26–29 dari 30 pengiriman bersamaan dijawab 500 |\n\n## 1. Yang salah bukan kodenya, tapi asumsinya\n\nKode awalnya nggak punya penjagaan apa pun, dan komentarnya menjelaskan kenapa penulisnya merasa\nitu cukup: *\"endpoint ini membalas 200 setelah transaksinya commit, jadi satu event sampai ke sini\ntepat satu kali.\"*\n\nDua kejadian di situ dianggap satu. Kenyataannya `session.commit()` dan jawaban 200 yang sampai\nke gateway adalah **dua kejadian terpisah**, dan yang kedua bisa gagal sendirian. Kalau gagal,\ngateway cuma tahu satu hal: dia belum menerima balasan.\n\n## 2. Unique key memperbaiki gejalanya, lalu memunculkan masalah baru — dan masalah baru itu benar\n\nTambahkan `UNIQUE (event_id)`. Dobelnya berhenti. Yang muncul:\n\n```\nstatus: [200, 500, 500, 500]\n1 dari 30 pengiriman bersamaan dijawab 2xx\nsqlalchemy.exc.IntegrityError: (psycopg2.errors.UniqueViolation)\nduplicate key value violates unique constraint \"ledger_entries_event_id_key\"\n```\n\nIni yang bikin orang mengira unique key itu bukan solusi. Padahal yang salah adalah **reaksi kita\nterhadap konfliknya**: kita memperlakukannya sebagai kegagalan, padahal dari sudut pandang gateway,\n\"event ini sudah pernah saya kirim dan sudah pernah diterima\" adalah hasil yang sukses.\n\nPerhatikan juga apa yang **tidak** rusak waktu itu: barisnya tetap satu, saldonya tetap benar.\nJadi duplicate key bukan masalah data — dia masalah **jawaban**. Gateway nggak pernah tahu\npercobaan pertamanya berhasil, jadi dia kirim lagi, dapat 500 lagi, kirim lagi. Yang bertambah\nbukan barisnya, tapi error-nya.\n\n## 3. Keputusannya pindah ke database, dan jawabannya disimpan\n\nDua perubahan, dan dua-duanya soal tempat menyimpan:\n\n- Klaim eventnya dipindah ke satu statement, bukan \"periksa dulu lalu tulis\":\n\n  ```sql\n  INSERT INTO processed_events (event_id, request_fingerprint)\n  VALUES ($1, $2)\n  ON CONFLICT (event_id) DO NOTHING\n  RETURNING event_id;\n  ```\n\n  Kosong = bukan kita yang dapat. Ini \"periksa dan tulis\" dalam satu operasi, jadi nggak ada\n  jendela waktu yang bisa dilewati dua-duanya. Nggak ada `except` yang perlu ditulis, karena\n  konflik yang bisa diprediksi bukan pengecualian.\n\n- Jawaban pengiriman pertama disimpan di baris klaimnya, dan pengiriman ulang mengembalikannya apa\n  adanya.\n\n**Kalau kamu memilih \"periksa dulu di aplikasi, baru `INSERT`\", itu masih merah.** Bentuk ini\nkelihatan setara dan di tes manual memang hijau; di bawah beban, 30 pengiriman membaca tabelnya\nbersamaan, semuanya melihat \"belum ada\", semuanya menulis. 26–29 di antaranya ditangkap unique key\ndan jadi 500. Hasil akhirnya nggak dobel, tapi endpoint-mu berteriak di hampir semua pengiriman\nbersamaan.\n\n## 4. Saldo bukan soal idempotensi, tapi ketemu di lab yang sama\n\n`account.balance = account.balance + nominal` kelihatan seperti penjumlahan. Yang sebenarnya\nterjadi: baca nilai dari database, kirim balik nilai barunya. Tiga puluh transaksi yang membaca\nnilai yang sama akan menulis nilai yang sama — 40.000 dari 300.000 yang terselamatkan.\n\nDi SQL, satu statement `UPDATE accounts SET balance = balance + $1 WHERE id = $2` nggak punya\nmasalah itu: database yang memegang barisnya selama update. Pelajaran yang sama dengan tiga\nlangkah sebelumnya, dalam bentuk paling murni: **kalau datanya ada di database, keputusannya juga\ndi database.**\n\nSaya baru benar-benar paham bagian ini waktu retry-nya datang dari worker, bukan dari HTTP. Retry yang salah bukan cuma soal satu request dijawab dua kali: dia bisa datang jam tiga pagi, dari worker lain, untuk pekerjaan yang sudah selesai. Obatnya sama, dan sejak itu saya nggak pernah lagi menganggap \"dijalankan sekali\" sebagai jaminan.\n\n## Jebakan yang paling menggoda\n\n```python\ntry:\n    session.flush()\nexcept IntegrityError:\n    pass\n```\n\nTerlihat aman karena baris gandanya memang nggak jadi masuk. Yang nggak kelihatan: di Postgres,\nstatement yang gagal membatalkan **seluruh transaksi**. Statement berikutnya gagal dengan `25P02:\ncurrent transaction is aborted`, dan kalau kamu menangkap error itu juga lalu mengembalikan 200,\nkamu baru saja bilang \"beres\" untuk pekerjaan yang nggak terjadi. Kalau tetap mau ditangani, butuh\n`SAVEPOINT` supaya yang dibatalkan cuma statement itu.\n\nJebakan kedua: **memisahkan klaim dari pekerjaannya.** Kalau klaimnya di-commit lebih dulu, ada\njendela waktu di mana server boleh mati — dan sesudah itu eventnya tercatat sudah diproses\nsementara ledger dan saldonya kosong, dan semua pengiriman ulang sesudahnya dianggap duplikat.\nDobel kelihatan di laporan; **hilang nggak kelihatan.**\n\n## Yang belum dicakup lab ini\n\n- **Efek samping ke luar database.** Kalau handler-mu juga mengirim email atau memanggil API pihak\n  ketiga, satu transaksi nggak menolong: email nggak bisa di-rollback. Itu pola outbox.\n- **Fakta yang datang dari dua jalur.** Di lab ini satu pembayaran cuma punya satu jalur. Begitu ada\n  job rekonsiliasi yang menutup pembayaran yang webhook-nya telat, semua kunci yang dipakai di lab\n  ini berhenti bekerja — dan yang bekerja tinggal kunci yang berasal dari domainnya sendiri.\n  Itu [lab berikutnya](https:\u002F\u002Fgithub.com\u002Fizzudd\u002Finva-lab\u002Ftree\u002Fmain\u002F0003.reconciliation).\n","Menambah `UNIQUE` menyelesaikan dobelnya, lalu memunculkan masalah baru yang justru benar.\n- Konflik \"sudah ada\" bukan kegagalan: dari sudut pandang pengirim, itu hasil yang sukses.\n- Klaimnya harus satu statement (`INSERT ... ON CONFLICT DO NOTHING RETURNING`), bukan periksa dulu lalu tulis.\n- Jawaban pengiriman pertama disimpan dan dikembalikan apa adanya.\n- Angka yang berubah dihitung database, bukan dibaca-ubah-tulis di aplikasi.",{"id":112,"title":113,"slug":115},"\u003Cp>Yang paling menarik dari lab ini bukan jawabannya, tapi \u003Cstrong>urutan gagalnya\u003C\u002Fstrong>. Karena urutan itulah\nkenapa banyak orang menyimpulkan &quot;unique key nggak menyelesaikan apa-apa&quot; — padahal constraint-nya\nbenar, dan database-nya benar.\u003C\u002Fp>\n\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 21. Angka di kolom terakhir: berapa yang masih merah.\u003C\u002Fp>\n\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>varian\u003C\u002Fth>\n\u003Cth>merah\u003C\u002Fth>\n\u003Cth>yang masih merah\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\u003Ctr>\n\u003Ctd>kondisi awal\u003C\u002Ftd>\n\u003Ctd>\u003Cstrong>11 dari 21\u003C\u002Fstrong>\u003C\u002Ftd>\n\u003Ctd>dobel, saldo, jawaban, dan \u003Ccode>event_id\u003C\u002Fcode> yang dipakai ulang\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>(1) + \u003Ccode>UNIQUE (event_id)\u003C\u002Fcode> di database, handler tidak diubah\u003C\u002Ftd>\n\u003Ctd>\u003Cstrong>7 dari 21\u003C\u002Fstrong>\u003C\u002Ftd>\n\u003Ctd>dijawab 2xx, jawaban, saldo\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>(2) + klaim \u003Ccode>ON CONFLICT DO NOTHING\u003C\u002Fcode>, tapi konflik dijawab pesan\u003C\u002Ftd>\n\u003Ctd>\u003Cstrong>1 dari 21\u003C\u002Fstrong>\u003C\u002Ftd>\n\u003Ctd>body pengiriman ulang beda\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>(3) + jawaban pengiriman pertama disimpan di baris klaimnya\u003C\u002Ftd>\n\u003Ctd>\u003Cstrong>1 dari 21\u003C\u002Fstrong>\u003C\u002Ftd>\n\u003Ctd>saldo\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>(4) + saldo dihitung database, bukan dibaca-ubah-tulis di Python\u003C\u002Ftd>\n\u003Ctd>\u003Cstrong>0 dari 21\u003C\u002Fstrong>\u003C\u002Ftd>\n\u003Ctd>—\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>alternatif untuk (2): periksa dulu di aplikasi, baru \u003Ccode>INSERT\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Cstrong>3 dari 21\u003C\u002Fstrong>\u003C\u002Ftd>\n\u003Ctd>26–29 dari 30 pengiriman bersamaan dijawab 500\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\u003C\u002Ftable>\n\u003Ch2 id=\"1-yang-salah-bukan-kodenya-tapi-asumsinya\">1. Yang salah bukan kodenya, tapi asumsinya\u003C\u002Fh2>\n\u003Cp>Kode awalnya nggak punya penjagaan apa pun, dan komentarnya menjelaskan kenapa penulisnya merasa\nitu cukup: \u003Cem>&quot;endpoint ini membalas 200 setelah transaksinya commit, jadi satu event sampai ke sini\ntepat satu kali.&quot;\u003C\u002Fem>\u003C\u002Fp>\n\u003Cp>Dua kejadian di situ dianggap satu. Kenyataannya \u003Ccode>session.commit()\u003C\u002Fcode> dan jawaban 200 yang sampai\nke gateway adalah \u003Cstrong>dua kejadian terpisah\u003C\u002Fstrong>, dan yang kedua bisa gagal sendirian. Kalau gagal,\ngateway cuma tahu satu hal: dia belum menerima balasan.\u003C\u002Fp>\n\u003Ch2 id=\"2-unique-key-memperbaiki-gejalanya-lalu-memunculkan-masalah-baru-dan-masalah-baru-itu-benar\">2. Unique key memperbaiki gejalanya, lalu memunculkan masalah baru — dan masalah baru itu benar\u003C\u002Fh2>\n\u003Cp>Tambahkan \u003Ccode>UNIQUE (event_id)\u003C\u002Fcode>. Dobelnya berhenti. Yang muncul:\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\">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=\"status%3A%20%5B200%2C%20500%2C%20500%2C%20500%5D%0A1%20dari%2030%20pengiriman%20bersamaan%20dijawab%202xx%0Asqlalchemy.exc.IntegrityError%3A%20(psycopg2.errors.UniqueViolation)%0Aduplicate%20key%20value%20violates%20unique%20constraint%20%22ledger_entries_event_id_key%22\" 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>status: [200, 500, 500, 500]\n1 dari 30 pengiriman bersamaan dijawab 2xx\nsqlalchemy.exc.IntegrityError: (psycopg2.errors.UniqueViolation)\nduplicate key value violates unique constraint \"ledger_entries_event_id_key\"\u003C\u002Fcode>\u003C\u002Fpre>\n  \u003C\u002Fdiv>\n\u003C\u002Fdiv>\u003Cp>Ini yang bikin orang mengira unique key itu bukan solusi. Padahal yang salah adalah \u003Cstrong>reaksi kita\nterhadap konfliknya\u003C\u002Fstrong>: kita memperlakukannya sebagai kegagalan, padahal dari sudut pandang gateway,\n&quot;event ini sudah pernah saya kirim dan sudah pernah diterima&quot; adalah hasil yang sukses.\u003C\u002Fp>\n\u003Cp>Perhatikan juga apa yang \u003Cstrong>tidak\u003C\u002Fstrong> rusak waktu itu: barisnya tetap satu, saldonya tetap benar.\nJadi duplicate key bukan masalah data — dia masalah \u003Cstrong>jawaban\u003C\u002Fstrong>. Gateway nggak pernah tahu\npercobaan pertamanya berhasil, jadi dia kirim lagi, dapat 500 lagi, kirim lagi. Yang bertambah\nbukan barisnya, tapi error-nya.\u003C\u002Fp>\n\u003Ch2 id=\"3-keputusannya-pindah-ke-database-dan-jawabannya-disimpan\">3. Keputusannya pindah ke database, dan jawabannya disimpan\u003C\u002Fh2>\n\u003Cp>Dua perubahan, dan dua-duanya soal tempat menyimpan:\u003C\u002Fp>\n\u003Cul>\n\u003Cli>\u003Cp>Klaim eventnya dipindah ke satu statement, bukan &quot;periksa dulu lalu tulis&quot;:\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\">SQL\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=\"INSERT%20INTO%20processed_events%20(event_id%2C%20request_fingerprint)%0AVALUES%20(%241%2C%20%242)%0AON%20CONFLICT%20(event_id)%20DO%20NOTHING%0ARETURNING%20event_id%3B\" 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:#F97583\">INSERT INTO\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> processed_events (event_id, request_fingerprint)\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">VALUES\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> ($\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">1\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">, $\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">2\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">)\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">ON\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> CONFLICT (event_id) DO NOTHING\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">RETURNING event_id;\u003C\u002Fspan>\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n  \u003C\u002Fdiv>\n\u003C\u002Fdiv>\u003Cp>Kosong = bukan kita yang dapat. Ini &quot;periksa dan tulis&quot; dalam satu operasi, jadi nggak ada\njendela waktu yang bisa dilewati dua-duanya. Nggak ada \u003Ccode>except\u003C\u002Fcode> yang perlu ditulis, karena\nkonflik yang bisa diprediksi bukan pengecualian.\u003C\u002Fp>\n\u003C\u002Fli>\n\u003Cli>\u003Cp>Jawaban pengiriman pertama disimpan di baris klaimnya, dan pengiriman ulang mengembalikannya apa\nadanya.\u003C\u002Fp>\n\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>\u003Cstrong>Kalau kamu memilih &quot;periksa dulu di aplikasi, baru \u003Ccode>INSERT\u003C\u002Fcode>&quot;, itu masih merah.\u003C\u002Fstrong> Bentuk ini\nkelihatan setara dan di tes manual memang hijau; di bawah beban, 30 pengiriman membaca tabelnya\nbersamaan, semuanya melihat &quot;belum ada&quot;, semuanya menulis. 26–29 di antaranya ditangkap unique key\ndan jadi 500. Hasil akhirnya nggak dobel, tapi endpoint-mu berteriak di hampir semua pengiriman\nbersamaan.\u003C\u002Fp>\n\u003Ch2 id=\"4-saldo-bukan-soal-idempotensi-tapi-ketemu-di-lab-yang-sama\">4. Saldo bukan soal idempotensi, tapi ketemu di lab yang sama\u003C\u002Fh2>\n\u003Cp>\u003Ccode>account.balance = account.balance + nominal\u003C\u002Fcode> kelihatan seperti penjumlahan. Yang sebenarnya\nterjadi: baca nilai dari database, kirim balik nilai barunya. Tiga puluh transaksi yang membaca\nnilai yang sama akan menulis nilai yang sama — 40.000 dari 300.000 yang terselamatkan.\u003C\u002Fp>\n\u003Cp>Di SQL, satu statement \u003Ccode>UPDATE accounts SET balance = balance + $1 WHERE id = $2\u003C\u002Fcode> nggak punya\nmasalah itu: database yang memegang barisnya selama update. Pelajaran yang sama dengan tiga\nlangkah sebelumnya, dalam bentuk paling murni: \u003Cstrong>kalau datanya ada di database, keputusannya juga\ndi database.\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Cp>Saya baru benar-benar paham bagian ini waktu retry-nya datang dari worker, bukan dari HTTP. Retry yang salah bukan cuma soal satu request dijawab dua kali: dia bisa datang jam tiga pagi, dari worker lain, untuk pekerjaan yang sudah selesai. Obatnya sama, dan sejak itu saya nggak pernah lagi menganggap &quot;dijalankan sekali&quot; sebagai jaminan.\u003C\u002Fp>\n\u003Ch2 id=\"jebakan-yang-paling-menggoda\">Jebakan yang paling menggoda\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\">PYTHON\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=\"try%3A%0A%20%20%20%20session.flush()%0Aexcept%20IntegrityError%3A%0A%20%20%20%20pass\" 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:#F97583\">try\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">:\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    session.flush()\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">except\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> IntegrityError:\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    pass\u003C\u002Fspan>\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n  \u003C\u002Fdiv>\n\u003C\u002Fdiv>\u003Cp>Terlihat aman karena baris gandanya memang nggak jadi masuk. Yang nggak kelihatan: di Postgres,\nstatement yang gagal membatalkan \u003Cstrong>seluruh transaksi\u003C\u002Fstrong>. Statement berikutnya gagal dengan \u003Ccode>25P02: current transaction is aborted\u003C\u002Fcode>, dan kalau kamu menangkap error itu juga lalu mengembalikan 200,\nkamu baru saja bilang &quot;beres&quot; untuk pekerjaan yang nggak terjadi. Kalau tetap mau ditangani, butuh\n\u003Ccode>SAVEPOINT\u003C\u002Fcode> supaya yang dibatalkan cuma statement itu.\u003C\u002Fp>\n\u003Cp>Jebakan kedua: \u003Cstrong>memisahkan klaim dari pekerjaannya.\u003C\u002Fstrong> Kalau klaimnya di-commit lebih dulu, ada\njendela waktu di mana server boleh mati — dan sesudah itu eventnya tercatat sudah diproses\nsementara ledger dan saldonya kosong, dan semua pengiriman ulang sesudahnya dianggap duplikat.\nDobel kelihatan di laporan; \u003Cstrong>hilang nggak kelihatan.\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Ch2 id=\"yang-belum-dicakup-lab-ini\">Yang belum dicakup lab ini\u003C\u002Fh2>\n\u003Cul>\n\u003Cli>\u003Cstrong>Efek samping ke luar database.\u003C\u002Fstrong> Kalau handler-mu juga mengirim email atau memanggil API pihak\nketiga, satu transaksi nggak menolong: email nggak bisa di-rollback. Itu pola outbox.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Fakta yang datang dari dua jalur.\u003C\u002Fstrong> Di lab ini satu pembayaran cuma punya satu jalur. Begitu ada\njob rekonsiliasi yang menutup pembayaran yang webhook-nya telat, semua kunci yang dipakai di lab\nini berhenti bekerja — dan yang bekerja tinggal kunci yang berasal dari domainnya sendiri.\nItu \u003Ca href=\"https:\u002F\u002Fgithub.com\u002Fizzudd\u002Finva-lab\u002Ftree\u002Fmain\u002F0003.reconciliation\">lab berikutnya\u003C\u002Fa>.\u003C\u002Fli>\n\u003C\u002Ful>\n","\u003Cp>Menambah \u003Ccode>UNIQUE\u003C\u002Fcode> menyelesaikan dobelnya, lalu memunculkan masalah baru yang justru benar.\u003C\u002Fp>\n\u003Cul>\n\u003Cli>Konflik &quot;sudah ada&quot; bukan kegagalan: dari sudut pandang pengirim, itu hasil yang sukses.\u003C\u002Fli>\n\u003Cli>Klaimnya harus satu statement (\u003Ccode>INSERT ... ON CONFLICT DO NOTHING RETURNING\u003C\u002Fcode>), bukan periksa dulu lalu tulis.\u003C\u002Fli>\n\u003Cli>Jawaban pengiriman pertama disimpan dan dikembalikan apa adanya.\u003C\u002Fli>\n\u003Cli>Angka yang berubah dihitung database, bukan dibaca-ubah-tulis di aplikasi.\u003C\u002Fli>\n\u003C\u002Ful>\n",[186,204,217,230,249,267],{"id":187,"title":188,"title_en":8,"slug":189,"excerpt":190,"excerpt_en":8,"has_translation":11,"published_at":191,"reading_time":192,"hidden_from_listing":11,"tags":193,"thumbnail_image":8,"og_image":195,"series":196,"authors":201},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,[150,194],"tuple","\u002Fog\u002Fpython-tuples.png",{"id":197,"title":198,"title_en":199,"slug":200},30,"Koleksi dan String","Collections and Strings","python-collections",[202],{"authors_id":203},{"id":107,"name":108,"slug":109,"role":110,"profile_picture":8},{"id":205,"title":206,"title_en":8,"slug":207,"excerpt":208,"excerpt_en":8,"has_translation":11,"published_at":209,"reading_time":192,"hidden_from_listing":11,"tags":210,"thumbnail_image":8,"og_image":212,"series":213,"authors":214},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,211],"set","\u002Fog\u002Fpython-sets.png",{"id":197,"title":198,"title_en":199,"slug":200},[215],{"authors_id":216},{"id":107,"name":108,"slug":109,"role":110,"profile_picture":8},{"id":218,"title":219,"title_en":8,"slug":220,"excerpt":221,"excerpt_en":8,"has_translation":11,"published_at":222,"reading_time":192,"hidden_from_listing":11,"tags":223,"thumbnail_image":8,"og_image":225,"series":226,"authors":227},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,224],"dictionary","\u002Fog\u002Fpython-dictionary.png",{"id":197,"title":198,"title_en":199,"slug":200},[228],{"authors_id":229},{"id":107,"name":108,"slug":109,"role":110,"profile_picture":8},{"id":231,"title":232,"title_en":8,"slug":233,"excerpt":234,"excerpt_en":8,"has_translation":11,"published_at":235,"reading_time":13,"hidden_from_listing":11,"tags":236,"thumbnail_image":8,"og_image":243,"series":244,"authors":246},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",[237,238,239,240,241,242],"rag","cloud-run","cloud-tasks","gcp","serverless","arsitektur","\u002Fog\u002Frag-pisah-jalur-query-dan-ingestion.png",{"id":245,"title":32,"title_en":33,"slug":34},8,[247],{"authors_id":248},{"id":107,"name":108,"slug":109,"role":110,"profile_picture":8},{"id":250,"title":251,"title_en":8,"slug":252,"excerpt":253,"excerpt_en":8,"has_translation":11,"published_at":254,"reading_time":192,"hidden_from_listing":11,"tags":255,"thumbnail_image":8,"og_image":262,"series":263,"authors":264},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",[256,257,258,259,260,261],"multi-agent","langgraph","autogen","orkestrasi","checkpoint","llm","\u002Fog\u002Fmulti-agen-linear-atau-grafik.png",{"id":245,"title":32,"title_en":33,"slug":34},[265],{"authors_id":266},{"id":107,"name":108,"slug":109,"role":110,"profile_picture":8},{"id":268,"title":269,"title_en":8,"slug":270,"excerpt":271,"excerpt_en":8,"has_translation":11,"published_at":272,"reading_time":192,"hidden_from_listing":11,"tags":273,"thumbnail_image":8,"og_image":279,"series":280,"authors":281},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",[237,274,275,276,277,278],"vector-search","bm25","hybrid-search","retrieval","embedding","\u002Fog\u002Fkenapa-rag-cuma-cosine-similarity-sering-nyasar.png",{"id":245,"title":32,"title_en":33,"slug":34},[282],{"authors_id":283},{"id":107,"name":108,"slug":109,"role":110,"profile_picture":8},[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. Yang salah bukan kodenya, tapi asumsinya","1-yang-salah-bukan-kodenya-tapi-asumsinya",{"text":293,"depth":287,"id":294},"2. Unique key memperbaiki gejalanya, lalu memunculkan masalah baru — dan masalah baru itu benar","2-unique-key-memperbaiki-gejalanya-lalu-memunculkan-masalah-baru-dan-masalah-baru-itu-benar",{"text":296,"depth":287,"id":297},"3. Keputusannya pindah ke database, dan jawabannya disimpan","3-keputusannya-pindah-ke-database-dan-jawabannya-disimpan",{"text":299,"depth":287,"id":300},"4. Saldo bukan soal idempotensi, tapi ketemu di lab yang sama","4-saldo-bukan-soal-idempotensi-tapi-ketemu-di-lab-yang-sama",{"text":302,"depth":287,"id":303},"Jebakan yang paling menggoda","jebakan-yang-paling-menggoda",{"text":305,"depth":287,"id":306},"Yang belum dicakup lab ini","yang-belum-dicakup-lab-ini",1793246466958]