Event sourcing: state sebagai replay events, proses kons, dual-write problem dan transactional outbox, snapshotting, replay dan audit trail — outbox table plus relay job di NestJS scheduler, Fiber ticker goroutine, Laravel queue worker schedule — praktik event stream OrderPlaced ItemAdded rebuild state dan outbox publish aman

Dua pola terakhir trio event menjawab dua masalah berbeda. Event sourcing menyimpan state bukan sebagai baris terakhir, melainkan sebagai rangkaian peristiwa — membuka audit trail sempurna dan time-travel. Transactional Outbox memperbaiki lubang kecil namun fatal di episode 21: bagaimana menjamin event benar-benar terkirim bersamaan dengan commit data.
TRADISIONAL : tabel orders -> UPDATE row (nilai lama HILANG)
EVENT SOURCING:
stream order#o1:
[1] OrderPlaced{customerId, items:[A x1]}
[2] ItemAdded{sku:B, qty:2}
[3] ItemRemoved{sku:A}
state sekarang = fold/replay semua event dari awalManfaatnya:
Biayanya jujur untuk disebut: query butuh proyeksi, evolusi schema event butuh upcasting, dan volume event besar butuh snapshotting — simpan state hasil fold tiap N event, replay hanya dari snapshot terakhir.
Bug klasik EDA: commit DB sukses, publish broker gagal (atau sebaliknya) → sistem tidak sinkron. Solusinya elegan:
Event tersimpan di tabel outbox dalam transaksi yang sama dengan data — atomik pasti. Relay (scheduler/ticker/worker) mengangkutnya ke broker. Konsumen tetap harus idempotent (ep.21) karena relay bisa dobel-kirim.
// Simpan bersama transaksi:
await this.uow.transaction(async () => {
await this.orders.save(order); // state tradisional ATAU stream
await this.outbox.append(order.pullEvents()); // events masuk satu transaksi
});
// Relay via @nestjs/schedule:
@Interval(1000)
async relay() {
const batch = await this.outbox.unsent(50);
for (const e of batch) {
await this.broker.publish(e.type, e.payload); // at-least-once
await this.outbox.markSent(e.id);
}
}function rebuild(events: StoredEvent[]): OrderState {
return events.reduce<OrderState>(
(s, e) => match (e.type) {
'OrderPlaced' => place(s, e),
'ItemAdded' => addItem(s, e),
'OrderShipped' => ship(s),
default => s,
},
emptyState(),
);
}Target outline: simpan event stream OrderPlaced/ItemAdded + rebuild state, dan outbox untuk publish aman.
1. Tabel event_store(stream_id, version, type, payload JSONB, occurred_at)
Tabel outbox(id PK, type, payload, created_at, sent_at NULL)
2. PlaceOrder use case:
a. append OrderPlaced ke event_store + outbox DALAM SATU transaksi
b. tambah ItemAdded x2 lewat method aggregate
3. Rebuild test: baca stream order o1 -> fold -> assert total/status
4. Snapshotting mini: simpan snapshot tiap 5 event;
rebuild mulai dari snapshot + sisa event (ukur selisih waktu!)
5. Relay: jalankan scheduler/ticker/worker ->
amati event sampai broker (consumer log ep.21)
6. Uji kegagalan: matikan broker saat relay jalan ->
event TETAP di outbox (sent_at masih null) ->
nyalakan broker -> terkirim. Itu bukti atomicity.Warning
Event sourcing adalah komitmen arsitektural besar. Terapkan pada modul yang AUDIT-nya bernilai mahal (ledger, order) — jangan pada seluruh sistem. Modul lain cukup menerima published events.
Rangkuman episode ini:
Episode 23 menjaga semuanya tetap benar: Testing Strategy per Arsitektur — piramida test, unit domain murni, mock port, integration adapter, contract test, dan architecture tests. Sampai jumpa!