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

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".
Aggregate = klaster entity+VO yang berubah bersama, dilindungi satu pintu: aggregate root.
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.
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 | Application Service | |
|---|---|---|
| Berisi | Logika bisnis lintas aggregate | Orkestrasi use case + infrastruktur |
| Contoh | PricingService.hitungDiskon() | PlaceOrderUseCase.execute() |
| Tahu tentang DB/email? | Tidak | Ya (via port) |
Aturan cepat: kalau logika cocok hidup di aggregate → taruh di aggregate; butuh koordinasi beberapa aggregate → domain service; mengatur alur + port → application service.
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.
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.
Rangkuman episode ini:
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!