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:
- Audit trail lengkap — tahu persis apa yang terjadi dan kapan
- Time travel — bisa rebuild state di titik waktu manapun
- Debugging mudah — replay event untuk menemukan masalah
- Event replay — jalankan ulang event untuk update proyeksi
Kapan pakai CQRS + Event Sourcing:
Cocok untuk sistem dengan:
- Kebutuhan audit trail yang ketat (perbankan, kesehatan, hukum)
- Read/write yang sangat berbeda skala dan kompleksitasnya
- Kebutuhan time-travel dan history lengkap
Jangan pakai jika: sistem sederhana, CRUD biasa, atau team belum familiar — kompleksitasnya tinggi.
🎭 Analogi sehari-hari: Buku rekening tabungan vs buku saldo.
- Traditional state: kamu cuma punya buku saldo "Rp 600.000" — gak tau gimana sampai sini
- Event sourcing: kamu punya buku rekening yang catat tiap transaksi: deposit Rp 500rb, tarik Rp 100rb, deposit Rp 200rb. Saldo = sum dari history. History = source of truth, saldo = derived
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:
- Premature CQRS = kompleksitas 5x lipat untuk app sederhana. Cuma pakai kalau read-write sangat berbeda
- Eventual consistency confusion — write success ≠ langsung visible di read. UI harus design awareness
- Event schema evolution = event lama versi 1, baru versi 2. Wajib backward compat reader
- Snapshot lupa = replay 10jt event setiap query = lambat. Snapshot setiap N event
- Lost events karena projection bug → corrupt state. Pakai durable broker + replay capability
🎯 CQRS only vs CQRS + ES:
- CQRS only = simple separation read/write model. Cocok kalau read query rumit
- CQRS + Event Sourcing = complete history. Cocok kalau audit + time travel matter
- Hindari kalau team < 10 orang atau domain CRUD biasa
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.