CQRS & Event Sourcing — System Design

CQRS (Command Query Responsibility Segregation) adalah pola yang memisahkan operasi tulis (Command) dari operasi baca (Query) menjadi model yang berbeda. Masala

CQRS (Command Query Responsibility Segregation) adalah pola yang memisahkan operasi tulis (Command) dari operasi baca (Query) menjadi model yang berbeda.

Masalah dengan model tunggal:

// Satu model untuk semua operasi
class OrderService {
  createOrder(data) { /* tulis ke DB */ }
  getOrder(id) { /* baca dari DB */ }
  getOrdersForDashboard() { /* baca kompleks dengan banyak JOIN */ }
  getOrderHistory() { /* baca dengan filter, sort, paginasi */ }
}

// Masalah: query baca yang kompleks sering bertentangan dengan
// model tulis yang dioptimasi untuk konsistensi

Dengan CQRS — pisahkan command dan query:

┌──────────────────┐     ┌──────────────────┐
│  Command Side    │     │   Query Side     │
│  (Write Model)   │     │  (Read Model)    │
│                  │     │                  │
│ CreateOrder      │     │ GetOrder         │
│ UpdateOrder      │     │ GetDashboard     │
│ CancelOrder      │     │ GetHistory       │
│                  │     │                  │
│  Write DB        │     │  Read DB         │
│  (normalized)    │     │  (denormalized)  │
└──────────────────┘     └──────────────────┘
         │                        ↑
         └──── sync via events ───┘

Implementasi CQRS:

// Command handler — operasi tulis
class OrderCommandHandler {
  async handle(command) {
    if (command.type === "CreateOrder") {
      const order = await db.orders.create(command.data);
      // Publish event untuk update read model
      await eventBus.publish("order.created", order);
      return order.id;
    }
  }
}

// Query handler — operasi baca (read model terpisah)
class OrderQueryHandler {
  async getOrderDetails(orderId) {
    // Baca dari read DB yang sudah denormalized
    // Tidak perlu JOIN karena sudah flat
    return await readDb.orderViews.findById(orderId);
  }

  async getDashboard(userId) {
    return await readDb.dashboardViews.findByUser(userId);
  }
}

Event Sourcing — simpan event, bukan state:

Alih-alih menyimpan state terkini, simpan semua event yang terjadi. State saat ini adalah hasil replay semua event.

// Event store — sumber kebenaran
const events = [
  { type: "AccountOpened", accountId: 1, balance: 0, timestamp: "..." },
  { type: "MoneyDeposited", accountId: 1, amount: 500000, timestamp: "..." },
  { type: "MoneyWithdrawn", accountId: 1, amount: 100000, timestamp: "..." },
  { type: "MoneyDeposited", accountId: 1, amount: 200000, timestamp: "..." },
];

// Rebuild state dengan replay events
function rebuildAccount(events) {
  return events.reduce((state, event) => {
    switch (event.type) {
      case "AccountOpened":
        return { ...state, balance: 0 };
      case "MoneyDeposited":
        return { ...state, balance: state.balance + event.amount };
      case "MoneyWithdrawn":
        return { ...state, balance: state.balance - event.amount };
    }
  }, {});
}
// Result: { balance: 600000 }

Keuntungan Event Sourcing:

Kapan pakai CQRS + Event Sourcing:

Cocok untuk sistem dengan:

Jangan pakai jika: sistem sederhana, CRUD biasa, atau team belum familiar — kompleksitasnya tinggi.

<title>CQRS + Event Sourcing</title> Client (write) Command Handler Event Store (append-only) Projection (SQL view) Projection (Elasticsearch) Projection (Redis cache) Client (query) Command append event replay/apply Query Write side → Event Store → Read projections (berbagai bentuk, optimized per query) Event store adalah source of truth. Setiap projection dibangun ulang dengan replay events.
Command side menerima write, event tersimpan di event store, projection update read model (SQL, search, cache), query side membaca dari projection yang paling cocok.

🎭 Analogi sehari-hari: Buku rekening tabungan vs buku saldo.

Bank pakai pendekatan ini — gak boleh lupa transaksi apapun karena audit trail wajib.

💡 Mengapa CQRS + ES populer di domain finance/legal? Compliance regulator wajib immutable audit trail. Setiap perubahan = event tercatat selamanya. Kalau cuma simpan state terkini, history hilang. Plus: bisa rebuild state ke titik waktu manapun ("apa saldo si A tanggal 1 Januari pukul 14:30?").

⚠️ Jebakan klasik:

🎯 CQRS only vs CQRS + ES:

TL;DR: CQRS = pisah model write (Command) dari read (Query). Event Sourcing = simpan event sebagai source of truth, state = derived. Powerful untuk audit, time-travel, complex read. Kompleks — pakai cuma kalau perlu.