Belajar Software Architecture and Design Patterns - DDD Tactical Design
Episode 19 of 28

Belajar Software Architecture and Design Patterns - DDD Tactical Design

DDD tactical design: aggregate dan aggregate root sebagai consistency boundary, entity vs value object revisited, domain events, domain service vs application service, invariant enforcement — aggregate class Order di NestJS dengan dispatch event, struct Fiber dengan event slice, rich domain Laravel terpisah dari Eloquent — praktik Order add item checkout cancel

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

Pendahuluan

Strategic design (ep.18) menarik garis batasnya; tactical design mengisi isi batas itu dengan objek yang benar. Bintang utamanya adalah aggregate — unit konsistensi yang menjadi jawaban atas pertanyaan "kapan transaksi dan aturan bisnis boleh menyentuh apa".

Konsep

Aggregate & Aggregate Root

Aggregate = klaster entity+VO yang berubah bersama, dilindungi satu pintu: aggregate root.

text
1. Root adalah SATU-SATUNYA pintu masuk: luar hanya memanggil method root
2. Invarian lintas anggota DIJAGA di root - tak bisa dibobol dari luar
3. Referensi antar aggregate pakai ID, bukan objek langsung
   (Order menyimpan CustomerId, bukan objek Customer)
4. Satu transaksi = satu aggregate. Butuh dua? -> eventual via domain event
5. Aggregate kecil. Besar = lambat + konflik konkurensi tinggi."

Contoh konkret: Order (root) berisi OrderLine (entity lokal) dan Money (VO). Aturan "total tidak boleh minus" dan "tidak bisa addItem setelah paid" hidup di Order — tidak ada jalur lain untuk melanggarnya karena luar tak bisa menyentuh OrderLine langsung.

Entity vs VO Revisited & Domain Events

Dari episode 8: entity punya identitas (OrderLine #7 bisa berganti qty), VO dibedakan nilai (Money(50000,'IDR') identik dengan instance lain bernilai sama). Di dalam aggregate: anggota dengan identitas lokal = entity; sisanya VO.

Domain event = fakta masa lampau yang bermakna bisnis (OrderPlaced, OrderCancelled) — dilepas oleh root saat invarian penting terjadi. Ia menjadi jembatan antar-aggregate tanpa coupling langsung (dan fondasi CQRS/EDA ep.20–21).

Domain Service vs Application Service

Domain ServiceApplication Service
BerisiLogika bisnis lintas aggregateOrkestrasi use case + infrastruktur
ContohPricingService.hitungDiskon()PlaceOrderUseCase.execute()
Tahu tentang DB/email?TidakYa (via port)

Aturan cepat: kalau logika cocok hidup di aggregate → taruh di aggregate; butuh koordinasi beberapa aggregate → domain service; mengatur alur + port → application service.

Real-World Implementasi

export class Order {
  private lines: OrderLine[] = [];
  private status: 'draft' | 'placed' | 'cancelled' = 'draft';
  private events: DomainEvent[] = [];
 
  private constructor(public readonly id: string,
                      public readonly customerId: string) {}
 
  static place(customerId: string): Order {
    const o = new Order(newOrderId(), customerId);
    o.events.push(new OrderPlaced(o.id));
    return o;
  }
 
  addItem(sku: string, qty: number, price: Money): void {
    if (this.status !== 'draft')
      throw new OrderLockedError(this.id);        // INVARIANT dijaga di sini
    this.lines.push(OrderLine.of(sku, qty, price));
  }
 
  cancel(reason: string): void {
    if (this.status === 'placed' && !this.canCancel())
      throw new CannotCancelError(this.id);
    this.status = 'cancelled';
    this.events.push(new OrderCancelled(this.id, reason));
  }
 
  pullEvents(): DomainEvent[] {                   // dipublish oleh app layer
    const out = this.events; this.events = []; return out;
  }
  total(): Money { return this.lines.reduce((m, l) => m.add(l.subtotal()), Money.zero('IDR')); }
}
// Application service: simpan + publish order.pullEvents() SETELAH commit.

Ketiganya menegakkan aturan yang sama: satu-satunya cara mengubah order adalah method perilaku root; invarian mustahil dilanggar dari luar.

Praktik

Target outline: aggregate Order (add item, checkout, cancel) dengan invariant dijaga di root — ketiga stack.

1. addItem pada draft            -> OK, total bertambah benar
2. addItem setelah placed        -> ERROR OrderLocked
3. checkout: draft dgn >= 1 item -> status placed + event OrderPlaced
4. checkout: draft kosong        -> ERROR EmptyOrder (invariant!)
5. cancel setelah shipped        -> ERROR CannotCancel
6. pullEvents dua kali           -> kedua kali KOSONG (event tidak dobel)
 
Ujian konsistensi: tulis test ini SEKALI sebagai daftar perilaku,
implementasikan di NestJS/Fiber/Laravel - hasil harus semantik identik.

Tip

Test aggregate seperti ini adalah unit test tercepat & paling stabil di seluruh sistem: murni memori, milidetik per kasus, dan tak pernah rusak karena refactor infrastruktur.

Penutup

Rangkuman episode ini:

  • Aggregate = consistency boundary; root satu pintu; referensi antar-aggregate via ID; satu transaksi satu aggregate.
  • Domain event menjembatani aggregate tanpa coupling; domain service vs application service dibedakan isinya.
  • Aggregate Order direalisasikan identik di tiga stack — perilaku, invarian, dan event-nya semantik sama.

Episode 20 membuka pintu arsitektur asimetris: CQRS — command bus/query bus, read model denormalisasi, dan kapan CQRS-lite cukup. Sampai jumpa!

Belajar Software Architecture and Design Patterns - DDD Tactical Design | Belajar Software Architecture and Design Patterns