[{"data":1,"prerenderedAt":161},["ShallowReactive",2],{"site-header":3,"site-footer-license":42,"article-jawaban-konflik-bukan-error-with-latest":46},{"navigations":4},[5,20,37],{"collection":6,"item":7},"series",{"__typename":6,"title":8,"slug":9,"children_series":10},"Artikel","all",[11,14,17],{"title":12,"slug":13},"Algoritma dan Struktur Data","algoritma-struktur-data",{"title":15,"slug":16},"Kecerdasan Artifisial","kecerdasan-artifisial",{"title":18,"slug":19},"Konsep Dasar Pemrograman","konsep-dasar-pemrograman",{"collection":6,"item":21},{"__typename":6,"title":22,"slug":23,"children_series":24},"Tutorial","tutorial",[25,28,31,34],{"title":26,"slug":27},"Belajar Java","belajar-java",{"title":29,"slug":30},"Belajar Pascal","belajar-pascal",{"title":32,"slug":33},"Belajar Golang","belajar-golang",{"title":35,"slug":36},"Belajar Python","belajar-python",{"collection":6,"item":38},{"__typename":6,"title":39,"slug":40,"children_series":41},"Lab","lab",[],{"name":43,"url":44,"show":45},"CC BY-SA","https://creativecommons.org/licenses/by-sa/4.0/",false,{"article":47,"renderedBody":92,"renderedTakeaway":93,"latestArticles":94,"toc":138},{"prerequisite":48,"id":52,"title":53,"slug":54,"excerpt":55,"published_at":56,"thumbnail_image":56,"tags":57,"reading_time":62,"takeway":63,"body":64,"authors":65,"series":71},{"id":49,"title":50,"slug":51},"114","Saldo Naik Empat Kali untuk Satu Pembayaran","saldo-naik-empat-kali-untuk-satu-pembayaran","115","Jawabannya: Konflik Bukan 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.",null,[58,59,60,61,40],"idempotency","webhook","postgres","database",4,"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.","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://github.com/izzudd/inva-lab/tree/main/reconciliation).\n",[66],{"authors_id":67},{"name":68,"slug":69,"role":70,"profile_picture":56},"Daffa Izzuddin","izzudd","Writer",{"id":72,"title":39,"slug":40,"articles":73,"parent_series":56},"29",[74,82,85],{"id":75,"title":76,"slug":77,"excerpt":78,"thumbnail_image":56,"tags":79},"104","Query-nya Tinggal 2, Tapi Response-nya Masih 1 Detik","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.",[61,80,81],"performance","backend",{"id":49,"title":50,"slug":51,"excerpt":83,"thumbnail_image":56,"tags":84},"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.",[58,59,60,61,40],{"id":86,"title":87,"slug":88,"excerpt":89,"thumbnail_image":56,"tags":90},"116","Webhook-nya Telat, Job Rekonsiliasinya Keburu Jalan","webhook-telat-job-rekonsiliasi-keburu-jalan","Jalur webhook sudah idempotent. Lalu ada job rekonsiliasi pagi, dan satu pembayaran tercatat dua kali dari dua jalur yang dua-duanya benar. Coba tebak kuncinya salah di mana.",[58,91,59,60,40],"reconciliation","\u003Cp>Yang paling menarik dari lab ini bukan jawabannya, tapi \u003Cstrong>urutan gagalnya\u003C/strong>. Karena urutan itulah\nkenapa banyak orang menyimpulkan &quot;unique key nggak menyelesaikan apa-apa&quot; — padahal constraint-nya\nbenar, dan database-nya benar.\u003C/p>\n\u003Ch2 id=\"hasil-pengukuran\">Hasil pengukuran\u003C/h2>\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/p>\n\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>varian\u003C/th>\n\u003Cth>merah\u003C/th>\n\u003Cth>yang masih merah\u003C/th>\n\u003C/tr>\n\u003C/thead>\n\u003Ctbody>\u003Ctr>\n\u003Ctd>kondisi awal\u003C/td>\n\u003Ctd>\u003Cstrong>11 dari 21\u003C/strong>\u003C/td>\n\u003Ctd>dobel, saldo, jawaban, dan \u003Ccode>event_id\u003C/code> yang dipakai ulang\u003C/td>\n\u003C/tr>\n\u003Ctr>\n\u003Ctd>(1) + \u003Ccode>UNIQUE (event_id)\u003C/code> di database, handler tidak diubah\u003C/td>\n\u003Ctd>\u003Cstrong>7 dari 21\u003C/strong>\u003C/td>\n\u003Ctd>dijawab 2xx, jawaban, saldo\u003C/td>\n\u003C/tr>\n\u003Ctr>\n\u003Ctd>(2) + klaim \u003Ccode>ON CONFLICT DO NOTHING\u003C/code>, tapi konflik dijawab pesan\u003C/td>\n\u003Ctd>\u003Cstrong>1 dari 21\u003C/strong>\u003C/td>\n\u003Ctd>body pengiriman ulang beda\u003C/td>\n\u003C/tr>\n\u003Ctr>\n\u003Ctd>(3) + jawaban pengiriman pertama disimpan di baris klaimnya\u003C/td>\n\u003Ctd>\u003Cstrong>1 dari 21\u003C/strong>\u003C/td>\n\u003Ctd>saldo\u003C/td>\n\u003C/tr>\n\u003Ctr>\n\u003Ctd>(4) + saldo dihitung database, bukan dibaca-ubah-tulis di Python\u003C/td>\n\u003Ctd>\u003Cstrong>0 dari 21\u003C/strong>\u003C/td>\n\u003Ctd>—\u003C/td>\n\u003C/tr>\n\u003Ctr>\n\u003Ctd>alternatif untuk (2): periksa dulu di aplikasi, baru \u003Ccode>INSERT\u003C/code>\u003C/td>\n\u003Ctd>\u003Cstrong>3 dari 21\u003C/strong>\u003C/td>\n\u003Ctd>26–29 dari 30 pengiriman bersamaan dijawab 500\u003C/td>\n\u003C/tr>\n\u003C/tbody>\u003C/table>\n\u003Ch2 id=\"1-yang-salah-bukan-kodenya-tapi-asumsinya\">1. Yang salah bukan kodenya, tapi asumsinya\u003C/h2>\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/em>\u003C/p>\n\u003Cp>Dua kejadian di situ dianggap satu. Kenyataannya \u003Ccode>session.commit()\u003C/code> dan jawaban 200 yang sampai\nke gateway adalah \u003Cstrong>dua kejadian terpisah\u003C/strong>, dan yang kedua bisa gagal sendirian. Kalau gagal,\ngateway cuma tahu satu hal: dia belum menerima balasan.\u003C/p>\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/h2>\n\u003Cp>Tambahkan \u003Ccode>UNIQUE (event_id)\u003C/code>. Dobelnya berhenti. Yang muncul:\u003C/p>\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/40 inline-block\">\u003C/span>\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#ffbd2e] border border-black/40 inline-block\">\u003C/span>\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#27c93f] border border-black/40 inline-block\">\u003C/span>\n      \u003Cspan class=\"ml-2 font-mono font-bold text-xs uppercase tracking-wider text-gray-300\">TEXT\u003C/span>\n    \u003C/div>\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/path>\n      \u003C/svg>\n      \u003Cspan class=\"copy-text\">Copy\u003C/span>\n    \u003C/button>\n  \u003C/div>\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/code>\u003C/pre>\n  \u003C/div>\n\u003C/div>\u003Cp>Ini yang bikin orang mengira unique key itu bukan solusi. Padahal yang salah adalah \u003Cstrong>reaksi kita\nterhadap konfliknya\u003C/strong>: 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/p>\n\u003Cp>Perhatikan juga apa yang \u003Cstrong>tidak\u003C/strong> rusak waktu itu: barisnya tetap satu, saldonya tetap benar.\nJadi duplicate key bukan masalah data — dia masalah \u003Cstrong>jawaban\u003C/strong>. Gateway nggak pernah tahu\npercobaan pertamanya berhasil, jadi dia kirim lagi, dapat 500 lagi, kirim lagi. Yang bertambah\nbukan barisnya, tapi error-nya.\u003C/p>\n\u003Ch2 id=\"3-keputusannya-pindah-ke-database-dan-jawabannya-disimpan\">3. Keputusannya pindah ke database, dan jawabannya disimpan\u003C/h2>\n\u003Cp>Dua perubahan, dan dua-duanya soal tempat menyimpan:\u003C/p>\n\u003Cul>\n\u003Cli>\u003Cp>Klaim eventnya dipindah ke satu statement, bukan &quot;periksa dulu lalu tulis&quot;:\u003C/p>\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/40 inline-block\">\u003C/span>\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#ffbd2e] border border-black/40 inline-block\">\u003C/span>\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#27c93f] border border-black/40 inline-block\">\u003C/span>\n      \u003Cspan class=\"ml-2 font-mono font-bold text-xs uppercase tracking-wider text-gray-300\">SQL\u003C/span>\n    \u003C/div>\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/path>\n      \u003C/svg>\n      \u003Cspan class=\"copy-text\">Copy\u003C/span>\n    \u003C/button>\n  \u003C/div>\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/span>\u003Cspan style=\"color:#E1E4E8\"> processed_events (event_id, request_fingerprint)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">VALUES\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> ($\u003C/span>\u003Cspan style=\"color:#79B8FF\">1\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, $\u003C/span>\u003Cspan style=\"color:#79B8FF\">2\u003C/span>\u003Cspan style=\"color:#E1E4E8\">)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">ON\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> CONFLICT (event_id) DO NOTHING\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">RETURNING event_id;\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n  \u003C/div>\n\u003C/div>\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/code> yang perlu ditulis, karena\nkonflik yang bisa diprediksi bukan pengecualian.\u003C/p>\n\u003C/li>\n\u003Cli>\u003Cp>Jawaban pengiriman pertama disimpan di baris klaimnya, dan pengiriman ulang mengembalikannya apa\nadanya.\u003C/p>\n\u003C/li>\n\u003C/ul>\n\u003Cp>\u003Cstrong>Kalau kamu memilih &quot;periksa dulu di aplikasi, baru \u003Ccode>INSERT\u003C/code>&quot;, itu masih merah.\u003C/strong> 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/p>\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/h2>\n\u003Cp>\u003Ccode>account.balance = account.balance + nominal\u003C/code> 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/p>\n\u003Cp>Di SQL, satu statement \u003Ccode>UPDATE accounts SET balance = balance + $1 WHERE id = $2\u003C/code> 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/strong>\u003C/p>\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/p>\n\u003Ch2 id=\"jebakan-yang-paling-menggoda\">Jebakan yang paling menggoda\u003C/h2>\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/40 inline-block\">\u003C/span>\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#ffbd2e] border border-black/40 inline-block\">\u003C/span>\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#27c93f] border border-black/40 inline-block\">\u003C/span>\n      \u003Cspan class=\"ml-2 font-mono font-bold text-xs uppercase tracking-wider text-gray-300\">PYTHON\u003C/span>\n    \u003C/div>\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/path>\n      \u003C/svg>\n      \u003Cspan class=\"copy-text\">Copy\u003C/span>\n    \u003C/button>\n  \u003C/div>\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/span>\u003Cspan style=\"color:#E1E4E8\">:\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    session.flush()\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">except\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> IntegrityError:\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    pass\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n  \u003C/div>\n\u003C/div>\u003Cp>Terlihat aman karena baris gandanya memang nggak jadi masuk. Yang nggak kelihatan: di Postgres,\nstatement yang gagal membatalkan \u003Cstrong>seluruh transaksi\u003C/strong>. Statement berikutnya gagal dengan \u003Ccode>25P02: current transaction is aborted\u003C/code>, 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/code> supaya yang dibatalkan cuma statement itu.\u003C/p>\n\u003Cp>Jebakan kedua: \u003Cstrong>memisahkan klaim dari pekerjaannya.\u003C/strong> 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/strong>\u003C/p>\n\u003Ch2 id=\"yang-belum-dicakup-lab-ini\">Yang belum dicakup lab ini\u003C/h2>\n\u003Cul>\n\u003Cli>\u003Cstrong>Efek samping ke luar database.\u003C/strong> Kalau handler-mu juga mengirim email atau memanggil API pihak\nketiga, satu transaksi nggak menolong: email nggak bisa di-rollback. Itu pola outbox.\u003C/li>\n\u003Cli>\u003Cstrong>Fakta yang datang dari dua jalur.\u003C/strong> 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://github.com/izzudd/inva-lab/tree/main/reconciliation\">lab berikutnya\u003C/a>.\u003C/li>\n\u003C/ul>\n","\u003Cp>Menambah \u003Ccode>UNIQUE\u003C/code> menyelesaikan dobelnya, lalu memunculkan masalah baru yang justru benar.\u003C/p>\n\u003Cul>\n\u003Cli>Konflik &quot;sudah ada&quot; bukan kegagalan: dari sudut pandang pengirim, itu hasil yang sukses.\u003C/li>\n\u003Cli>Klaimnya harus satu statement (\u003Ccode>INSERT ... ON CONFLICT DO NOTHING RETURNING\u003C/code>), bukan periksa dulu lalu tulis.\u003C/li>\n\u003Cli>Jawaban pengiriman pertama disimpan dan dikembalikan apa adanya.\u003C/li>\n\u003Cli>Angka yang berubah dihitung database, bukan dibaca-ubah-tulis di aplikasi.\u003C/li>\n\u003C/ul>\n",[95,105,116,118,126,136],{"id":96,"title":97,"slug":98,"excerpt":99,"thumbnail_image":56,"tags":100},"113","Garbage Collector dituduh bikin aplikasi lemot. Seberapa adil tuduhan itu?","garbage-collector-dituduh-bikin-aplikasi-lemot","GC sering jadi tersangka utama tiap aplikasi melambat, dan banyak yang percaya dia jalan pakai timer tiap beberapa detik. Dua-duanya nggak sepenuhnya benar. Yuk bedah cara kerja GC dari dalam, plus satu pengecualian yang bikin Discord kesakitan tiap 2 menit. 🤔",[81,80,101,102,103,104],"golang","jvm","garbage-collector","memory",{"id":106,"title":107,"slug":108,"excerpt":109,"thumbnail_image":56,"tags":110},"111","Rust Masuk Kernel Linux: Revolusi Nyata atau Cuma Euforia RIIR?","rust-di-kernel-linux-revolusi-atau-cuma-hype","Selama lebih dari 30 tahun kernel Linux cuma mau bahasa C, lalu tiba-tiba Rust resmi masuk di Linux 6.1. Itu revolusi beneran atau cuma kebawa euforia meme Rewrite It In Rust? Yuk lihat datanya, bukan cuma opininya.",[111,112,113,114,115],"rust","linux","kernel","system-programming","memory-safety",{"id":49,"title":50,"slug":51,"excerpt":83,"thumbnail_image":56,"tags":117},[58,59,60,61,40],{"id":119,"title":120,"slug":121,"excerpt":122,"thumbnail_image":56,"tags":123},"112","Aplikasimu lambat, dan kamu udah kepikiran ganti bahasa. Tunggu dulu","aplikasimu-lambat-dan-kamu-udah-kepikiran-ganti-bahasa-tunggu-dulu","Ada endpoint yang grafiknya spike tiap beberapa menit, dan kepikiran buat pindah dari Go ke Rust? Discord dan Cloudflare beneran melakukannya — tapi cuma setelah kehabisan cara lain dan punya datanya. Yuk, bedah gejala mana yang beneran butuh rewrite, dan mana yang cuma butuh profiling. 🤔",[81,80,101,111,124,125],"system-design","profiling",{"id":127,"title":128,"slug":129,"excerpt":130,"thumbnail_image":56,"tags":131},"118","Belajar Idempotency dari Stok yang Keburu Kebeli","pub-sub-sendiri-lalu-belajar-idempotency","Tugas pertama saya di kerjaan pertama: sistem yang kewalahan menerima webhook. Saya putuskan membangun pub/sub sendiri dari nol, dan pelajaran yang akhirnya datang bukan dari situ — tapi dari stok gudang yang keburu kebeli orang.",[58,132,133,134,135],"event-driven","celery","rabbitmq","post-mortem",{"id":86,"title":87,"slug":88,"excerpt":89,"thumbnail_image":56,"tags":137},[58,91,59,60,40],[139,143,146,149,152,155,158],{"text":140,"depth":141,"id":142},"Hasil pengukuran",2,"hasil-pengukuran",{"text":144,"depth":141,"id":145},"1. Yang salah bukan kodenya, tapi asumsinya","1-yang-salah-bukan-kodenya-tapi-asumsinya",{"text":147,"depth":141,"id":148},"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":150,"depth":141,"id":151},"3. Keputusannya pindah ke database, dan jawabannya disimpan","3-keputusannya-pindah-ke-database-dan-jawabannya-disimpan",{"text":153,"depth":141,"id":154},"4. Saldo bukan soal idempotensi, tapi ketemu di lab yang sama","4-saldo-bukan-soal-idempotensi-tapi-ketemu-di-lab-yang-sama",{"text":156,"depth":141,"id":157},"Jebakan yang paling menggoda","jebakan-yang-paling-menggoda",{"text":159,"depth":141,"id":160},"Yang belum dicakup lab ini","yang-belum-dicakup-lab-ini",1791651538336]