[{"data":1,"prerenderedAt":133},["ShallowReactive",2],{"site-header":3,"site-footer-license":37,"article-transactional-outbox-pattern-konsistensi-database-message-broker-with-latest":41},{"navigations":4},[5,20],{"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},"Konsep Dasar Pemrograman","konsep-dasar-pemrograman",{"title":18,"slug":19},"Kecerdasan Artifisial","kecerdasan-artifisial",{"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 Python","belajar-python",{"title":35,"slug":36},"Belajar Golang","belajar-golang",{"name":38,"url":39,"show":40},"CC BY-SA","https://creativecommons.org/licenses/by-sa/4.0/",false,{"article":42,"renderedBody":67,"renderedTakeaway":68,"latestArticles":69,"toc":109},{"id":43,"title":44,"slug":45,"excerpt":46,"published_at":47,"thumbnail_image":48,"tags":50,"reading_time":58,"takeway":59,"body":60,"authors":61,"series":58},"44","Transactional Outbox Pattern: Solusi Konsistensi Database dan Message Broker Tanpa 2PC","transactional-outbox-pattern-konsistensi-database-message-broker","Pernah nggak kalian lihat pesanan udah masuk database, tapi email konfirmasinya nggak pernah kekirim? Itu gejala *dual-write problem*. Yuk, kita bedah Transactional Outbox Pattern biar data dan *event* kalian nggak lagi berselisih paham!","2026-09-04T12:15:23.152Z",{"id":49},"e3c617fd-1486-4b82-b937-94fbec846c77",[51,52,53,54,55,56,57],"backend","microservices","architecture","database","message-broker","system-design","postgresql",null,"- Dual-write problem muncul karena nulis ke database dan *publish* ke *message broker* adalah dua operasi jaringan terpisah yang nggak bisa dijamin berhasil barengan.\n- Transactional Outbox Pattern menitipkan *event* ke tabel `outbox` di transaksi database yang sama, jadi nasibnya ikut atomik: tersimpan semua atau nggak sama sekali.\n- Tabel `outbox` bisa dikuras lewat *Polling Publisher* buat skala kecil sampai menengah, atau *Change Data Capture* (CDC) buat volume ribuan transaksi per detik.\n- Jaminan pengirimannya *at-least-once*, jadi *consumer* di sisi hilir wajib idempoten supaya *event* kembar nggak dieksekusi dua kali.","Ada satu pola pincang di kode *backend* yang hampir semua orang pernah tulis tanpa sadar. Simpan pesanan ke database dulu, terus *publish* *event*-nya ke *message broker*. Kelihatannya rapi, kan?\n\n```python\n# Skenario 1: simpan ke DB dulu, baru publish event\nasync def create_order(order_data):\n    async with db.transaction():\n        order = await db.insert_order(order_data)\n\n    # Apa yang terjadi kalau baris ini crash atau kena network timeout?\n    await message_broker.publish(\"order.created\", order.dict())\n```\n\nKarena khawatir pesannya nggak kekirim, sebagian dari kita lalu mutusin buat balikin urutannya.\n\n```python\n# Skenario 2: publish event dulu, baru simpan ke DB\nasync def create_order(order_data):\n    await message_broker.publish(\"order.created\", order_data)\n\n    # Apa yang terjadi kalau transaksi database gagal (constraint violation)?\n    async with db.transaction():\n        await db.insert_order(order_data)\n```\n\nDua-duanya nyimpen bom waktu yang sama, namanya **Dual-Write Problem**.\n\nDi sistem terdistribusi, kita emang nggak bisa menjamin dua operasi jaringan yang terpisah—nulis ke database SQL dan *publish* *event* ke *message broker* kayak [Apache Kafka](https://kafka.apache.org/) atau [RabbitMQ](https://www.rabbitmq.com/)—berhasil barengan tanpa mekanisme konsistensi khusus.\n\nKalau skenario 1 gagal pas *publish*, pesanannya udah kejual di database tapi email konfirmasi dan instruksi gudang nggak pernah terpicu. Sebaliknya di skenario 2, kalau commit database-nya gagal, *event* udah terlanjur beredar ke seluruh ekosistem *microservices* padahal pesanannya sebenernya nggak pernah ada! 😬\n\nDulu, jawaban teoretis buat masalah ini adalah **Two-Phase Commit (2PC)** alias XA Transactions. Cuma, di dunia *cloud* modern 2PC sering dihindari: lambat, ribet, dan sebagian *message broker* modern nggak mau mendukungnya.\n\nTerus solusinya apa dong? Di sinilah **Transactional Outbox Pattern** naik kelas jadi standar emas industri.\n\n## Menunggangi transaksi ACID database\n\nKunci utama *Outbox Pattern* itu sederhana aja: jangan ngobrol sama dua sistem sekaligus dalam satu alur *request* bisnis. Cukup ngobrol sama database lokal kalian sendiri!\n\nDatabase relasional kayak [PostgreSQL](https://www.postgresql.org/) atau MySQL udah punya transaksi ACID yang solid dan teruji puluhan tahun. Kalau kita bisa nyelipin *event* yang mau dikirim ke dalam transaksi database yang sama dengan data bisnisnya, kita dapat jaminan atomik: semua tersimpan, atau nggak ada sama sekali.\n\nKayak nulis dua hal di satu lembar nota belanja. Kalau notanya robek di tengah jalan, dua-duanya nggak terbaca—nggak mungkin yang satu selamat dan satunya hilang entah ke mana.\n\nAlurnya sebenernya elegan:\n\n1. Bikin tabel tambahan bernama `outbox` di database yang sama dengan tabel bisnis kalian (misal tabel `orders`).\n2. Pas *request* `create_order` masuk, aplikasi mulai transaksi database:\n   - Insert baris baru ke tabel `orders`.\n   - Insert payload *event* yang mau dikirim ke tabel `outbox`.\n   - Lakukan `COMMIT`.\n3. Komponen terpisah (*Outbox Relay* / *worker*) baca baris yang belum terkirim dari tabel `outbox`, *publish* ke *message broker*, lalu tandai barisnya sebagai `processed` atau hapus.\n\n## Isi tabel outbox-nya kayak gimana?\n\nBerikut contoh skema DDL sederhana buat tabel *outbox* di PostgreSQL:\n\n```sql\nCREATE TABLE outbox_events (\n    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),\n    aggregate_type VARCHAR(64) NOT NULL, -- contoh: 'Order'\n    aggregate_id VARCHAR(64) NOT NULL,   -- contoh: 'ord-12345'\n    event_type VARCHAR(64) NOT NULL,     -- contoh: 'OrderCreated'\n    payload JSONB NOT NULL,              -- payload event dalam format JSON\n    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),\n    processed_at TIMESTAMPTZ NULL        -- timestamp ketika berhasil di-publish\n);\n\nCREATE INDEX idx_outbox_unprocessed ON outbox_events (created_at) WHERE processed_at IS NULL;\n```\n\nDengan indeks parsial (*partial index*) di atas, pencarian *event* yang belum diproses tetap kencang tanpa perlu *full table scan*.\n\n## Dua cara nguras outbox: polling vs CDC\n\nGimana caranya mindahin data dari tabel `outbox` ke *message broker*? Ada dua pendekatan yang umum dipakai.\n\n### Polling publisher, buat skala kecil sampai menengah\n\n*Background worker* sederhana—bisa berupa *goroutine* di Go, *worker* Celery/APScheduler di Python, atau *scheduler* periodik—jalanin *query* polling tiap beberapa detik:\n\n```python\n# Mengambil batch outbox yang belum terkirim dengan row-level lock\nasync def process_outbox():\n    async with db.transaction():\n        events = await db.fetch(\n            \"\"\"\n            SELECT id, event_type, payload\n            FROM outbox_events\n            WHERE processed_at IS NULL\n            ORDER BY created_at ASC\n            LIMIT 50\n            FOR UPDATE SKIP LOCKED\n            \"\"\"\n        )\n\n        for event in events:\n            await kafka_producer.send(topic=event['event_type'], value=event['payload'])\n            await db.execute(\n                \"UPDATE outbox_events SET processed_at = NOW() WHERE id = $1\",\n                event['id']\n            )\n```\n\nPemakaian `FOR UPDATE SKIP LOCKED` bikin kalian bisa ngejalanin beberapa *instance* *worker* *outbox* secara paralel tanpa saling berebut baris data (*race condition*).\n\n### Change Data Capture, buat skala masif\n\nKalau volume transaksinya udah nyampe ribuan per detik, polling terus-menerus ke database malah ngeberatin CPU database.\n\nSebagai gantinya, pakai alat CDC kayak [Debezium](https://debezium.io/). Debezium baca langsung *Write-Ahead Log* (WAL di Postgres atau binlog di MySQL) di level disk. Begitu ada baris baru mendarat di tabel `outbox`, Debezium langsung nangkep perubahannya dan nerusin ke topik Kafka secara *real-time* dengan latensi sub-detik, tanpa ngeberatin *query engine* database kalian!\n\n## Consumernya wajib idempoten, nggak bisa ditawar\n\n*Outbox Pattern* cuma ngasih jaminan pengiriman **At-Least-Once** (setidaknya sekali). Artinya, pesannya dijamin nggak bakal hilang, tapi bisa aja terkirim lebih dari sekali kalau ada crash di tengah proses *publish* dan update statusnya.\n\nMakanya setiap *consumer* di *microservices* hilir wajib dirancang idempoten:\n\n- Pakai `id` unik *event* dari *outbox* sebagai *idempotency key*.\n- Simpan ID *event* yang udah pernah diproses ke cache Redis atau tabel `processed_events` di sisi *consumer*.\n- Kalau *event* dengan ID yang sama balik datang, tinggal diabaikan dan langsung kirim ACK.\n\n## Jadi, kenapa harus pindah dari dual-write?\n\nBangun arsitektur *microservices* yang andal itu bukan soal ngindarin kegagalan jaringan. Justru soal ngerancang sistem yang tetap konsisten pas kegagalan jaringan—yang nggak mungkin dihindari itu—beneran kejadian.\n\nDengan Transactional Outbox Pattern, kalian bisa tidur nyenyak tanpa mikirin data pesanan hilang entah ke mana atau saldo pelanggan kepotong dua kali. Dua-duanya dijamin jalan bareng, atau nggak jalan sama sekali. Rapi, kan? 🎉\n\nSelamat ber-*outbox* ria, dan semoga *event* kalian selalu nyampe tepat waktu walau *broker*-nya lagi ngambek! 👋",[62],{"authors_id":63},{"name":64,"slug":65,"role":66,"profile_picture":58},"Inva","inva","Writer","\u003Cp>Ada satu pola pincang di kode \u003Cem>backend\u003C/em> yang hampir semua orang pernah tulis tanpa sadar. Simpan pesanan ke database dulu, terus \u003Cem>publish\u003C/em> \u003Cem>event\u003C/em>-nya ke \u003Cem>message broker\u003C/em>. Kelihatannya rapi, kan?\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\">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=\"%23%20Skenario%201%3A%20simpan%20ke%20DB%20dulu%2C%20baru%20publish%20event%0Aasync%20def%20create_order(order_data)%3A%0A%20%20%20%20async%20with%20db.transaction()%3A%0A%20%20%20%20%20%20%20%20order%20%3D%20await%20db.insert_order(order_data)%0A%0A%20%20%20%20%23%20Apa%20yang%20terjadi%20kalau%20baris%20ini%20crash%20atau%20kena%20network%20timeout%3F%0A%20%20%20%20await%20message_broker.publish(%22order.created%22%2C%20order.dict())\" 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:#6A737D\"># Skenario 1: simpan ke DB dulu, baru publish event\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">async\u003C/span>\u003Cspan style=\"color:#F97583\"> def\u003C/span>\u003Cspan style=\"color:#B392F0\"> create_order\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(order_data):\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    async\u003C/span>\u003Cspan style=\"color:#F97583\"> with\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> db.transaction():\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        order \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#F97583\"> await\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> db.insert_order(order_data)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">    # Apa yang terjadi kalau baris ini crash atau kena network timeout?\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    await\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> message_broker.publish(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"order.created\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, order.dict())\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n  \u003C/div>\n\u003C/div>\u003Cp>Karena khawatir pesannya nggak kekirim, sebagian dari kita lalu mutusin buat balikin urutannya.\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\">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=\"%23%20Skenario%202%3A%20publish%20event%20dulu%2C%20baru%20simpan%20ke%20DB%0Aasync%20def%20create_order(order_data)%3A%0A%20%20%20%20await%20message_broker.publish(%22order.created%22%2C%20order_data)%0A%0A%20%20%20%20%23%20Apa%20yang%20terjadi%20kalau%20transaksi%20database%20gagal%20(constraint%20violation)%3F%0A%20%20%20%20async%20with%20db.transaction()%3A%0A%20%20%20%20%20%20%20%20await%20db.insert_order(order_data)\" 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:#6A737D\"># Skenario 2: publish event dulu, baru simpan ke DB\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">async\u003C/span>\u003Cspan style=\"color:#F97583\"> def\u003C/span>\u003Cspan style=\"color:#B392F0\"> create_order\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(order_data):\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    await\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> message_broker.publish(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"order.created\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, order_data)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">    # Apa yang terjadi kalau transaksi database gagal (constraint violation)?\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    async\u003C/span>\u003Cspan style=\"color:#F97583\"> with\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> db.transaction():\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">        await\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> db.insert_order(order_data)\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n  \u003C/div>\n\u003C/div>\u003Cp>Dua-duanya nyimpen bom waktu yang sama, namanya \u003Cstrong>Dual-Write Problem\u003C/strong>.\u003C/p>\n\u003Cp>Di sistem terdistribusi, kita emang nggak bisa menjamin dua operasi jaringan yang terpisah—nulis ke database SQL dan \u003Cem>publish\u003C/em> \u003Cem>event\u003C/em> ke \u003Cem>message broker\u003C/em> kayak \u003Ca href=\"https://kafka.apache.org/\">Apache Kafka\u003C/a> atau \u003Ca href=\"https://www.rabbitmq.com/\">RabbitMQ\u003C/a>—berhasil barengan tanpa mekanisme konsistensi khusus.\u003C/p>\n\u003Cp>Kalau skenario 1 gagal pas \u003Cem>publish\u003C/em>, pesanannya udah kejual di database tapi email konfirmasi dan instruksi gudang nggak pernah terpicu. Sebaliknya di skenario 2, kalau commit database-nya gagal, \u003Cem>event\u003C/em> udah terlanjur beredar ke seluruh ekosistem \u003Cem>microservices\u003C/em> padahal pesanannya sebenernya nggak pernah ada! 😬\u003C/p>\n\u003Cp>Dulu, jawaban teoretis buat masalah ini adalah \u003Cstrong>Two-Phase Commit (2PC)\u003C/strong> alias XA Transactions. Cuma, di dunia \u003Cem>cloud\u003C/em> modern 2PC sering dihindari: lambat, ribet, dan sebagian \u003Cem>message broker\u003C/em> modern nggak mau mendukungnya.\u003C/p>\n\u003Cp>Terus solusinya apa dong? Di sinilah \u003Cstrong>Transactional Outbox Pattern\u003C/strong> naik kelas jadi standar emas industri.\u003C/p>\n\u003Ch2 id=\"menunggangi-transaksi-acid-database\">Menunggangi transaksi ACID database\u003C/h2>\n\u003Cp>Kunci utama \u003Cem>Outbox Pattern\u003C/em> itu sederhana aja: jangan ngobrol sama dua sistem sekaligus dalam satu alur \u003Cem>request\u003C/em> bisnis. Cukup ngobrol sama database lokal kalian sendiri!\u003C/p>\n\u003Cp>Database relasional kayak \u003Ca href=\"https://www.postgresql.org/\">PostgreSQL\u003C/a> atau MySQL udah punya transaksi ACID yang solid dan teruji puluhan tahun. Kalau kita bisa nyelipin \u003Cem>event\u003C/em> yang mau dikirim ke dalam transaksi database yang sama dengan data bisnisnya, kita dapat jaminan atomik: semua tersimpan, atau nggak ada sama sekali.\u003C/p>\n\u003Cp>Kayak nulis dua hal di satu lembar nota belanja. Kalau notanya robek di tengah jalan, dua-duanya nggak terbaca—nggak mungkin yang satu selamat dan satunya hilang entah ke mana.\u003C/p>\n\u003Cp>Alurnya sebenernya elegan:\u003C/p>\n\u003Col>\n\u003Cli>Bikin tabel tambahan bernama \u003Ccode>outbox\u003C/code> di database yang sama dengan tabel bisnis kalian (misal tabel \u003Ccode>orders\u003C/code>).\u003C/li>\n\u003Cli>Pas \u003Cem>request\u003C/em> \u003Ccode>create_order\u003C/code> masuk, aplikasi mulai transaksi database:\u003Cul>\n\u003Cli>Insert baris baru ke tabel \u003Ccode>orders\u003C/code>.\u003C/li>\n\u003Cli>Insert payload \u003Cem>event\u003C/em> yang mau dikirim ke tabel \u003Ccode>outbox\u003C/code>.\u003C/li>\n\u003Cli>Lakukan \u003Ccode>COMMIT\u003C/code>.\u003C/li>\n\u003C/ul>\n\u003C/li>\n\u003Cli>Komponen terpisah (\u003Cem>Outbox Relay\u003C/em> / \u003Cem>worker\u003C/em>) baca baris yang belum terkirim dari tabel \u003Ccode>outbox\u003C/code>, \u003Cem>publish\u003C/em> ke \u003Cem>message broker\u003C/em>, lalu tandai barisnya sebagai \u003Ccode>processed\u003C/code> atau hapus.\u003C/li>\n\u003C/ol>\n\u003Ch2 id=\"isi-tabel-outbox-nya-kayak-gimana\">Isi tabel outbox-nya kayak gimana?\u003C/h2>\n\u003Cp>Berikut contoh skema DDL sederhana buat tabel \u003Cem>outbox\u003C/em> di PostgreSQL:\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=\"CREATE%20TABLE%20outbox_events%20(%0A%20%20%20%20id%20UUID%20PRIMARY%20KEY%20DEFAULT%20gen_random_uuid()%2C%0A%20%20%20%20aggregate_type%20VARCHAR(64)%20NOT%20NULL%2C%20--%20contoh%3A%20'Order'%0A%20%20%20%20aggregate_id%20VARCHAR(64)%20NOT%20NULL%2C%20%20%20--%20contoh%3A%20'ord-12345'%0A%20%20%20%20event_type%20VARCHAR(64)%20NOT%20NULL%2C%20%20%20%20%20--%20contoh%3A%20'OrderCreated'%0A%20%20%20%20payload%20JSONB%20NOT%20NULL%2C%20%20%20%20%20%20%20%20%20%20%20%20%20%20--%20payload%20event%20dalam%20format%20JSON%0A%20%20%20%20created_at%20TIMESTAMPTZ%20NOT%20NULL%20DEFAULT%20NOW()%2C%0A%20%20%20%20processed_at%20TIMESTAMPTZ%20NULL%20%20%20%20%20%20%20%20--%20timestamp%20ketika%20berhasil%20di-publish%0A)%3B%0A%0ACREATE%20INDEX%20idx_outbox_unprocessed%20ON%20outbox_events%20(created_at)%20WHERE%20processed_at%20IS%20NULL%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\">CREATE\u003C/span>\u003Cspan style=\"color:#F97583\"> TABLE\u003C/span>\u003Cspan style=\"color:#B392F0\"> outbox_events\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> (\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    id UUID \u003C/span>\u003Cspan style=\"color:#F97583\">PRIMARY KEY\u003C/span>\u003Cspan style=\"color:#F97583\"> DEFAULT\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> gen_random_uuid(),\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    aggregate_type \u003C/span>\u003Cspan style=\"color:#F97583\">VARCHAR\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#79B8FF\">64\u003C/span>\u003Cspan style=\"color:#E1E4E8\">) \u003C/span>\u003Cspan style=\"color:#F97583\">NOT NULL\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#6A737D\">-- contoh: 'Order'\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    aggregate_id \u003C/span>\u003Cspan style=\"color:#F97583\">VARCHAR\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#79B8FF\">64\u003C/span>\u003Cspan style=\"color:#E1E4E8\">) \u003C/span>\u003Cspan style=\"color:#F97583\">NOT NULL\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,   \u003C/span>\u003Cspan style=\"color:#6A737D\">-- contoh: 'ord-12345'\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    event_type \u003C/span>\u003Cspan style=\"color:#F97583\">VARCHAR\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#79B8FF\">64\u003C/span>\u003Cspan style=\"color:#E1E4E8\">) \u003C/span>\u003Cspan style=\"color:#F97583\">NOT NULL\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,     \u003C/span>\u003Cspan style=\"color:#6A737D\">-- contoh: 'OrderCreated'\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    payload JSONB \u003C/span>\u003Cspan style=\"color:#F97583\">NOT NULL\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,              \u003C/span>\u003Cspan style=\"color:#6A737D\">-- payload event dalam format JSON\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    created_at \u003C/span>\u003Cspan style=\"color:#F97583\">TIMESTAMPTZ\u003C/span>\u003Cspan style=\"color:#F97583\"> NOT NULL\u003C/span>\u003Cspan style=\"color:#F97583\"> DEFAULT\u003C/span>\u003Cspan style=\"color:#F97583\"> NOW\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(),\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    processed_at \u003C/span>\u003Cspan style=\"color:#F97583\">TIMESTAMPTZ\u003C/span>\u003Cspan style=\"color:#F97583\"> NULL\u003C/span>\u003Cspan style=\"color:#6A737D\">        -- timestamp ketika berhasil di-publish\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">);\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">CREATE\u003C/span>\u003Cspan style=\"color:#F97583\"> INDEX\u003C/span>\u003Cspan style=\"color:#B392F0\"> idx_outbox_unprocessed\u003C/span>\u003Cspan style=\"color:#F97583\"> ON\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> outbox_events (created_at) \u003C/span>\u003Cspan style=\"color:#F97583\">WHERE\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> processed_at \u003C/span>\u003Cspan style=\"color:#F97583\">IS\u003C/span>\u003Cspan style=\"color:#F97583\"> NULL\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n  \u003C/div>\n\u003C/div>\u003Cp>Dengan indeks parsial (\u003Cem>partial index\u003C/em>) di atas, pencarian \u003Cem>event\u003C/em> yang belum diproses tetap kencang tanpa perlu \u003Cem>full table scan\u003C/em>.\u003C/p>\n\u003Ch2 id=\"dua-cara-nguras-outbox-polling-vs-cdc\">Dua cara nguras outbox: polling vs CDC\u003C/h2>\n\u003Cp>Gimana caranya mindahin data dari tabel \u003Ccode>outbox\u003C/code> ke \u003Cem>message broker\u003C/em>? Ada dua pendekatan yang umum dipakai.\u003C/p>\n\u003Ch3 id=\"polling-publisher-buat-skala-kecil-sampai-menengah\">Polling publisher, buat skala kecil sampai menengah\u003C/h3>\n\u003Cp>\u003Cem>Background worker\u003C/em> sederhana—bisa berupa \u003Cem>goroutine\u003C/em> di Go, \u003Cem>worker\u003C/em> Celery/APScheduler di Python, atau \u003Cem>scheduler\u003C/em> periodik—jalanin \u003Cem>query\u003C/em> polling tiap beberapa detik:\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\">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=\"%23%20Mengambil%20batch%20outbox%20yang%20belum%20terkirim%20dengan%20row-level%20lock%0Aasync%20def%20process_outbox()%3A%0A%20%20%20%20async%20with%20db.transaction()%3A%0A%20%20%20%20%20%20%20%20events%20%3D%20await%20db.fetch(%0A%20%20%20%20%20%20%20%20%20%20%20%20%22%22%22%0A%20%20%20%20%20%20%20%20%20%20%20%20SELECT%20id%2C%20event_type%2C%20payload%0A%20%20%20%20%20%20%20%20%20%20%20%20FROM%20outbox_events%0A%20%20%20%20%20%20%20%20%20%20%20%20WHERE%20processed_at%20IS%20NULL%0A%20%20%20%20%20%20%20%20%20%20%20%20ORDER%20BY%20created_at%20ASC%0A%20%20%20%20%20%20%20%20%20%20%20%20LIMIT%2050%0A%20%20%20%20%20%20%20%20%20%20%20%20FOR%20UPDATE%20SKIP%20LOCKED%0A%20%20%20%20%20%20%20%20%20%20%20%20%22%22%22%0A%20%20%20%20%20%20%20%20)%0A%0A%20%20%20%20%20%20%20%20for%20event%20in%20events%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20await%20kafka_producer.send(topic%3Devent%5B'event_type'%5D%2C%20value%3Devent%5B'payload'%5D)%0A%20%20%20%20%20%20%20%20%20%20%20%20await%20db.execute(%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22UPDATE%20outbox_events%20SET%20processed_at%20%3D%20NOW()%20WHERE%20id%20%3D%20%241%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20event%5B'id'%5D%0A%20%20%20%20%20%20%20%20%20%20%20%20)\" 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:#6A737D\"># Mengambil batch outbox yang belum terkirim dengan row-level lock\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">async\u003C/span>\u003Cspan style=\"color:#F97583\"> def\u003C/span>\u003Cspan style=\"color:#B392F0\"> process_outbox\u003C/span>\u003Cspan style=\"color:#E1E4E8\">():\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    async\u003C/span>\u003Cspan style=\"color:#F97583\"> with\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> db.transaction():\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        events \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#F97583\"> await\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> db.fetch(\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">            \"\"\"\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">            SELECT id, event_type, payload\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">            FROM outbox_events\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">            WHERE processed_at IS NULL\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">            ORDER BY created_at ASC\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">            LIMIT 50\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">            FOR UPDATE SKIP LOCKED\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">            \"\"\"\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        )\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">        for\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> event \u003C/span>\u003Cspan style=\"color:#F97583\">in\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> events:\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">            await\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> kafka_producer.send(\u003C/span>\u003Cspan style=\"color:#FFAB70\">topic\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\">event[\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'event_type'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">], \u003C/span>\u003Cspan style=\"color:#FFAB70\">value\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\">event[\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'payload'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">])\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">            await\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> db.execute(\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">                \"UPDATE outbox_events SET processed_at = NOW() WHERE id = $1\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">                event[\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'id'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">]\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">            )\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n  \u003C/div>\n\u003C/div>\u003Cp>Pemakaian \u003Ccode>FOR UPDATE SKIP LOCKED\u003C/code> bikin kalian bisa ngejalanin beberapa \u003Cem>instance\u003C/em> \u003Cem>worker\u003C/em> \u003Cem>outbox\u003C/em> secara paralel tanpa saling berebut baris data (\u003Cem>race condition\u003C/em>).\u003C/p>\n\u003Ch3 id=\"change-data-capture-buat-skala-masif\">Change Data Capture, buat skala masif\u003C/h3>\n\u003Cp>Kalau volume transaksinya udah nyampe ribuan per detik, polling terus-menerus ke database malah ngeberatin CPU database.\u003C/p>\n\u003Cp>Sebagai gantinya, pakai alat CDC kayak \u003Ca href=\"https://debezium.io/\">Debezium\u003C/a>. Debezium baca langsung \u003Cem>Write-Ahead Log\u003C/em> (WAL di Postgres atau binlog di MySQL) di level disk. Begitu ada baris baru mendarat di tabel \u003Ccode>outbox\u003C/code>, Debezium langsung nangkep perubahannya dan nerusin ke topik Kafka secara \u003Cem>real-time\u003C/em> dengan latensi sub-detik, tanpa ngeberatin \u003Cem>query engine\u003C/em> database kalian!\u003C/p>\n\u003Ch2 id=\"consumernya-wajib-idempoten-nggak-bisa-ditawar\">Consumernya wajib idempoten, nggak bisa ditawar\u003C/h2>\n\u003Cp>\u003Cem>Outbox Pattern\u003C/em> cuma ngasih jaminan pengiriman \u003Cstrong>At-Least-Once\u003C/strong> (setidaknya sekali). Artinya, pesannya dijamin nggak bakal hilang, tapi bisa aja terkirim lebih dari sekali kalau ada crash di tengah proses \u003Cem>publish\u003C/em> dan update statusnya.\u003C/p>\n\u003Cp>Makanya setiap \u003Cem>consumer\u003C/em> di \u003Cem>microservices\u003C/em> hilir wajib dirancang idempoten:\u003C/p>\n\u003Cul>\n\u003Cli>Pakai \u003Ccode>id\u003C/code> unik \u003Cem>event\u003C/em> dari \u003Cem>outbox\u003C/em> sebagai \u003Cem>idempotency key\u003C/em>.\u003C/li>\n\u003Cli>Simpan ID \u003Cem>event\u003C/em> yang udah pernah diproses ke cache Redis atau tabel \u003Ccode>processed_events\u003C/code> di sisi \u003Cem>consumer\u003C/em>.\u003C/li>\n\u003Cli>Kalau \u003Cem>event\u003C/em> dengan ID yang sama balik datang, tinggal diabaikan dan langsung kirim ACK.\u003C/li>\n\u003C/ul>\n\u003Ch2 id=\"jadi-kenapa-harus-pindah-dari-dual-write\">Jadi, kenapa harus pindah dari dual-write?\u003C/h2>\n\u003Cp>Bangun arsitektur \u003Cem>microservices\u003C/em> yang andal itu bukan soal ngindarin kegagalan jaringan. Justru soal ngerancang sistem yang tetap konsisten pas kegagalan jaringan—yang nggak mungkin dihindari itu—beneran kejadian.\u003C/p>\n\u003Cp>Dengan Transactional Outbox Pattern, kalian bisa tidur nyenyak tanpa mikirin data pesanan hilang entah ke mana atau saldo pelanggan kepotong dua kali. Dua-duanya dijamin jalan bareng, atau nggak jalan sama sekali. Rapi, kan? 🎉\u003C/p>\n\u003Cp>Selamat ber-\u003Cem>outbox\u003C/em> ria, dan semoga \u003Cem>event\u003C/em> kalian selalu nyampe tepat waktu walau \u003Cem>broker\u003C/em>-nya lagi ngambek! 👋\u003C/p>\n","\u003Cul>\n\u003Cli>Dual-write problem muncul karena nulis ke database dan \u003Cem>publish\u003C/em> ke \u003Cem>message broker\u003C/em> adalah dua operasi jaringan terpisah yang nggak bisa dijamin berhasil barengan.\u003C/li>\n\u003Cli>Transactional Outbox Pattern menitipkan \u003Cem>event\u003C/em> ke tabel \u003Ccode>outbox\u003C/code> di transaksi database yang sama, jadi nasibnya ikut atomik: tersimpan semua atau nggak sama sekali.\u003C/li>\n\u003Cli>Tabel \u003Ccode>outbox\u003C/code> bisa dikuras lewat \u003Cem>Polling Publisher\u003C/em> buat skala kecil sampai menengah, atau \u003Cem>Change Data Capture\u003C/em> (CDC) buat volume ribuan transaksi per detik.\u003C/li>\n\u003Cli>Jaminan pengirimannya \u003Cem>at-least-once\u003C/em>, jadi \u003Cem>consumer\u003C/em> di sisi hilir wajib idempoten supaya \u003Cem>event\u003C/em> kembar nggak dieksekusi dua kali.\u003C/li>\n\u003C/ul>\n",[70,83,95,98],{"id":71,"title":72,"slug":73,"excerpt":74,"thumbnail_image":75,"tags":77},"46","Mengapa C Mulai Ditinggalkan: Duel Filosofi Keamanan Rust vs Kesederhanaan Zig","mengapa-c-mulai-ditinggalkan-duel-filosofi-rust-vs-zig","Pernah denger kalau sekitar 70% celah keamanan kritis software modern itu asalnya dari bahasa C? Iya, bahasa yang udah jadi fondasi internet ini 😅. Yuk, kita bedah duel filosofi Rust vs Zig dan cari tahu mana yang pas buat proyek kalian!",{"id":76},"1942a5c9-c1f6-476d-9913-768f78d9d8eb",[78,79,80,81,82],"system-programming","rust","zig","c","memory-safety",{"id":84,"title":85,"slug":86,"excerpt":87,"thumbnail_image":88,"tags":90},"45","Rate Limiting Algorithms: Token Bucket vs Leaky Bucket vs Sliding Window","rate-limiting-algorithms-token-bucket-leaky-bucket-sliding-window","Udah tahu kenapa *rate limiting* itu wajib, tapi masih bingung beda Token Bucket, Leaky Bucket, Fixed Window, dan Sliding Window? Yuk, kita adu empat algoritma ini bareng-bareng dan cari tahu mana yang paling pas buat API kalian!",{"id":89},"0e0ed26d-20c6-443c-bde1-4a6200f6f801",[51,56,91,92,93,53,94],"rate-limiting","redis","api-gateway","traffict-management",{"id":43,"title":44,"slug":45,"excerpt":46,"thumbnail_image":96,"tags":97},{"id":49},[51,52,53,54,55,56,57],{"id":99,"title":100,"slug":101,"excerpt":102,"thumbnail_image":103,"tags":105},"43","Dead Letter Queue (DLQ) & Retry Policy: Menyelamatkan Pesan Nyasar di RabbitMQ Tanpa Bikin Worker Macet","dead-letter-queue-retry-policy-rabbitmq-resilient-messaging","Pernah nggak sih, *worker* kalian nolak satu pesan terus-terusan sampai CPU melonjak 100%? Itu namanya *poison message loop*, jebakan klasik di RabbitMQ. Yuk, kita bahas cara pasang *retry policy* dan *dead letter queue* yang bener!",{"id":104},"6c0a7ead-4841-4073-b29e-df7ae0f2aa3c",[106,55,51,53,52,107,108],"rabbitmq","async","python",[110,114,117,120,124,127,130],{"text":111,"depth":112,"id":113},"Menunggangi transaksi ACID database",2,"menunggangi-transaksi-acid-database",{"text":115,"depth":112,"id":116},"Isi tabel outbox-nya kayak gimana?","isi-tabel-outbox-nya-kayak-gimana",{"text":118,"depth":112,"id":119},"Dua cara nguras outbox: polling vs CDC","dua-cara-nguras-outbox-polling-vs-cdc",{"text":121,"depth":122,"id":123},"Polling publisher, buat skala kecil sampai menengah",3,"polling-publisher-buat-skala-kecil-sampai-menengah",{"text":125,"depth":122,"id":126},"Change Data Capture, buat skala masif","change-data-capture-buat-skala-masif",{"text":128,"depth":112,"id":129},"Consumernya wajib idempoten, nggak bisa ditawar","consumernya-wajib-idempoten-nggak-bisa-ditawar",{"text":131,"depth":112,"id":132},"Jadi, kenapa harus pindah dari dual-write?","jadi-kenapa-harus-pindah-dari-dual-write",1791045071514]