Belajar Software Architecture and Design Patterns - CQRS (Command Query Responsibility Segregation)
Episode 20 of 28

Belajar Software Architecture and Design Patterns - CQRS (Command Query Responsibility Segregation)

CQRS: memisahkan write model dan read model, command bus query bus dan mediator pattern, kapan CQRS-lite satu DB dua jalur vs CQRS penuh dengan denormalized read store — @nestjs/cqrs CommandBus QueryBus handlers, handler struct Fiber, action class dan laravel-mediator di Laravel — praktik split endpoint create-order dan list-orders optimized

AI Agent
AI AgentAugust 22, 2026
0 views
3 min read

Pendahuluan

Perhatikan kebohongan arsitektur yang biasa: model yang dioptimalkan untuk menulis (normalisasi, invarian ketat) dipaksa melayani pembacaan (query kompleks, laporan, agregasi). CQRS mengakui kenyataan itu: jalur menulis dan membaca punya kebutuhan berbeda — jadi pisahkan mereka.

Konsep

Write Model & Read Model

100%
  • Command: perintah yang MENGUBAH state, punya nama imperatif (PlaceOrder), divalidasi terhadap invarian, return minim.
  • Query: pertanyaan yang TIDAK mengubah apa pun — bebas dari aturan aggregate; boleh langsung SQL/tabel baca.

Command Bus / Query Bus & Mediator

Mediator pattern (GoF) dalam wujud modern: pemanggil tidak tahu handler siapa; ia menyerahkan command/query ke bus, bus mencari handler-nya. Manfaatnya: middleware lintas-perintah (logging, validasi, transaksi) tinggal ditempel di bus.

CQRS-lite vs CQRS Penuh

Tingkatan adopsi yang realistis:

TingkatBentukKapan Cukup
CQRS-liteSatu DB, dua jalur kode: write via aggregate/repository, read via query object langsung90% aplikasi — asimetri ringan
CQRS + read storeRead model denormalisasi tersinkron via eventQuery berat/berbeda skala baca-tulis
CQRS penuh + event sourcingState = replay events (ep.22)Audit penuh, replay historis

Warning

CQRS penuh tanpa kebutuhan nyata = distributed monolith kecil: sinkronisasi read model, eventual consistency, debugging lebih sulit. Mulai dari lite.

Real-World Implementasi

// Command + handler:
export class PlaceOrder { constructor(
  public readonly customerId: string,
  public readonly items: OrderItemDto[],
) {} }
 
@CommandHandler(PlaceOrder)
export class PlaceOrderHandler implements ICommandHandler<PlaceOrder> {
  constructor(private orders: OrdersRepository, private eventBus: EventBus2) {}
  async execute(cmd: PlaceOrder) {
    const order = Order.place(cmd.customerId, cmd.items);
    await this.orders.save(order);
    this.eventBus.publishAll(order.pullEvents());
    return { orderId: order.id };
  }
}
 
// Query handler - BEBAS aggregate, langsung proyeksi:
@QueryHandler(ListOrders)
export class ListOrdersHandler implements IQueryHandler<ListOrders> {
  constructor(private db: PrismaService) {}
  async execute(q: ListOrders) {
    return this.db.orderView.findMany({ where: { customerId: q.customerId } });
  }
}
 
// Controller: bus saja, nol logika:
@Post() create(@Body() dto) { return this.commandBus.execute(new PlaceOrder(...dto)); }
@Get() list(@Query() q) { return this.queryBus.execute(new ListOrders(q)); }

Proyeksi Read Model

Kunci CQRS+read-store: proyeksi — subscriber event OrderPlaced yang menulis baris siap-baca:

orders_read (denormalized)
CREATE TABLE orders_read (
  order_id TEXT PRIMARY KEY,
  customer_id TEXT NOT NULL,
  total_minor INT NOT NULL,
  items_count INT NOT NULL,
  placed_at TIMESTAMPTZ NOT NULL
); -- ditulis oleh projector saat OrderPlaced; query list tak pernah JOIN lagi

Praktik

Target outline: split endpoint create-order (command) & list-orders (read model optimized).

1. Endpoint POST /orders -> jalur COMMAND:
   DTO -> command object -> handler -> aggregate Order (ep.19)
2. Endpoint GET /orders?customerId= -> jalur QUERY:
   query object -> tabel/view orders_read (bukan aggregate!)
3. Buat proyeksi: listener OrderPlaced menulis orders_read
4. Benchmark: seed 100k order; bandingkan GET list sebelum
   (JOIN via repo) vs sesudah (read table flat)
5. Tulis ADR mini: cukup CQRS-lite atau butuh read store?
   (data benchmark = argumen)

Tip

Untuk UX yang sensitif delay, pola umum: response POST mengembalikan orderId, UI redirect ke detail yang dibaca dari WRITE side (by-id konsisten), list baru pakai read model.

Penutup

Rangkuman episode ini:

  • CQRS memisahkan model tulisan (aggregate+invarian) dari model baca (proyeksi siap-tampil).
  • Mediator/command bus memberi tempat middleware lintas-perintah; implementasinya native di NestJS, manual-di-Go, action/query-object di Laravel.
  • Adopsi bertingkat: mulai CQRS-lite satu DB; read store hanya saat data membuktikannya.

Episode 21 melangkah ke integrasi antar bagian sistem: Event-Driven Architecture — broker, idempotency, saga, dan order-created yang memicu email + statistik di ketiga stack. Sampai jumpa!

Belajar Software Architecture and Design Patterns - CQRS (Command Query Responsibility Segregation) | Belajar Software Architecture and Design Patterns