📚 DDD Bounded Context — Memecah Domain Tanpa Bikin Kekacauan
🎯 Tujuan Belajar
Setelah sesi ini, keputusan berikut jadi lebih baik: - Menentukan batas modul/service berdasarkan model domain, bukan berdasarkan tabel DB atau struktur tim. - Mengenali kapan satu class harus dipisah per context (Product sebagai katalog vs Product sebagai item gudang). - Kapan tidak pakai DDD (kebanyakan tim over-engineer di sini).
🧠 Penjelasan Inti — Masalah yang Diselesaikan
Masalah klasik: satu model User atau Product dipakai seluruh aplikasi. Mulai 5 fitur, class itu punya 40 field. Tim marketing butuh displayName, tim billing butuh taxId, tim gudang butuh weightKg, tim auth butuh passwordHash. Semua satu tabel, satu ORM entity, satu migrasi. Hasil: god object + coupling antar domain yang tak terlihat.
DDD jawab dengan satu ide: model itu bukan universal. Model itu kontekstual.
Bounded context = batas eksplisit di mana sebuah model punya satu makna tunggal dan konsisten. Di dalam batas: satu ubiquitous language, satu model, satu schema DB (idealnya). Di luar batas: model itu tidak berlaku.
Kunci: kata yang sama boleh berarti beda di context berbeda, dan itu sehat.
| Istilah | Sales Context | Warehouse Context | Billing Context |
|---|---|---|---|
Product |
SKU + harga + deskripsi marketing | SKU + berat + lokasi rak + stok | SKU + tarif pajak + akun revenue |
Customer |
lead/prospect + preferensi | alamat kirim + jadwal | NPWP + termin + credit limit |
Order |
quote → kontrak | work order picking | invoice + payment term |
Bukan duplikasi buruk — ini desain. Yang buruk: satu tabel products dengan 60 kolom nullable karena tiap divisi nambah sendiri.
⚙️ Cara Kerja Internal — Tiga Langkah Menemukan Batas
1. Event Storming (atau versi ringkasnya)
Tulis alur bisnis sebagai domain event (kata kerja lampau): OrderPlaced, StockReserved, PaymentCaptured, ShipmentDispatched. Lalu kelompokkan event yang:
- dipicu aktor/policy yang sama,
- memakai istilah yang saling konsisten,
- berubah bersamaan saat requirement berubah.
Heuristik paling andal: linguistic boundary. Kalau dalam satu alur satu kata mulai butuh qualifier ("product versi gudang"), itu sinyal batas context.
2. Uji Coupling — yang berubah bersama, tinggal bersama
Pertanyaan penguji: - Kalau tim A ubah aturan, apakah tim B wajib rilis? → kalau ya, satu context. - Apakah keduanya butuh transaksi ACID yang sama? → kuat, satu context (kecuali siap pakai saga). - Apakah keduanya butuh konsistensi instan pada data yang sama? → satu context.
3. Model Hubungan Antar-Context (Context Map)
Batas saja tidak cukup — harus ada pola integrasi:
| Pola | Arti | Kapan |
|---|---|---|
| Partnership | dua tim sukses/gagal bareng, koordinasi ketat | jarang, mahal, hanya untuk tim kecil yang benar-benar sinkron |
| Shared Kernel | sebagian model dipakai bersama, butuh approval gabungan | hindari kalau bisa; sering jadi sumber konflik |
| Customer–Supplier | downstream punya hak veto atas kontrak upstream | pola default yang sehat |
| Conformist | downstream ikut saja model upstream | aman untuk integrasi ke vendor/pihak lemah |
| Anti-Corruption Layer (ACL) | lapisan terjemahan, model upstream tidak bocor ke dalam | wajib saat integrasi ke legacy/vendor |
| Separate Ways | tidak integrasi sama sekali | sering pilihan terbaik yang dilupakan |
ACL adalah kerja paling sering di-skip dan paling sering harus dibayar mahal belakangan. Ini adapter yang menerjemahkan DTO upstream → model domain sendiri. Tanpa ACL, model vendor bocor ke domain inti dan jadi utang permanen.
Hubungan dengan Hexagonal / Clean Architecture
- Bounded context = batas organisasi masalah (domain).
- Hexagonal/Clean = batas teknis di dalam satu context (domain vs adapter I/O). Keduanya ortogonal. Satu context = satu hexagon. Jangan bingung: microservice belum tentu bounded context, dan bounded context belum tentu service terpisah.
🏗️ Diagram
┌──────────────────── Sales ────────────────────┐
│ Order, Quote, Customer(prospect) │
│ [REST/domain] ── [DB sales] │
└───────────────┬───────────────────────────────┘
│ OrderPlaced (event, async)
┌───────────────▼───────────────────────────────┐
│ Warehouse ┌──── ACL ────┐ │
│ StockItem,Picking │ translate │ │
│ [DB warehouse] └─────────────┘ │
└───────────────┬───────────────────────────────┘
│ StockReserved
┌───────────────▼───────────────────────────────┐
│ Billing │
│ Invoice, PaymentTerm │
│ [DB billing] │
└───────────────────────────────────────────────┘
Tiap kotak: model sendiri, schema sendiri, rilis sendiri.
Panah = kontrak (event atau API), bukan DB share.
⚖️ Trade-off & Kapan JANGAN Dipakai
Biaya bounded context: - Duplikasi data (Product ada di 3 tabel). Sinkronisasi = masalah baru. - Integrasi jadi eventual, bukan transaksional → butuh saga/outbox. - Debugging lintas context lebih berat (butuh correlation ID, tracing). - Tim butuh kontrak eksplisit, versioning event, komitmen koordinasi.
JANGAN pakai bounded context bila: 1. Satu tim, satu deployable, domain kecil (< ~10 aggregate). Batas modul dalam monolith modular sudah cukup — pakai folder/namespace + larangan import antar modul (ditegakkan lewat static analysis/ArchUnit), bukan service terpisah. 2. Data butuh konsistensi kuat lintas domain (mis. double-entry accounting). Ini secara alami satu context. 3. Domain masih belum dipahami. Batas salah lebih mahal daripada tidak ada batas. Mulai monolith modular, belah setelah event storming kedua. 4. Tim belum punya budaya kontrak API + observability. Microservices tanpa itu = distributed monolith (lihat failure mode).
Sinyal batas salah (harus dibelah ulang):
- Dua context "berbeda" yang selalu rilis bareng dan share DB → sebenarnya satu.
- Satu context yang punya 5 tim dan 3 model Customer berbeda di dalamnya → sebenarnya harus dipecah.
- Banyak ACL yang isinya cuma 3 baris mapping → batasnya mungkin artifisial.
🏭 Contoh Produksi & Failure Mode
-
Distributed Monolith — paling umum. Service terpisah tapi: share DB, deploy harus berurutan, satu service down bikin semua down, tracing tidak ada. Gejala: "kita punya 12 microservice tapi rilisnya harus bareng semua." Akar: batas ditarik dari struktur organisasi/tabel, bukan domain, dan integrasi memakai shared DB alih-alih kontrak.
-
Shared Kernel yang membusuk — dua tim sepakat pakai
common-domain.jar. Enam bulan kemudian semua perubahan harus approval dua tim, rilis saling blokir. Solusi: ACL atau pisahkan model. -
Tanpa ACL ke payment gateway — DTO vendor (field
status_code: "00",settlement_time) bocor ke seluruh codebase. Vendor ganti API versi → 40 file berubah, termasuk logika bisnis. Ini kegagalan klasik yang bisa 100% dicegah. -
Event tanpa schema versioning — upstream rename field di payload
StockReserved, downstream crash diam-diam karena event bus fire-and-forget. Wajib: schema registry atau aturan versioning + consumer tolerance (ignore unknown field, default untuk field hilang). -
Aggregate terlalu besar —
Ordermemuat seluruhCustomerdan semuaProductentity → lock contention, transaksi panjang. Aturan: aggregate hanya berisi yang butuh invariant transaksional; referensi ke aggregate lain pakai ID saja.
Catatan kejujuran epistemik: angka "berapa service ideal" tidak ada — tergantung ukuran tim dan biaya koordinasi. Yang pasti: setiap batas menambah biaya integrasi; pastikan manfaat isolasi perubahan melebihi biaya itu.
⚠️ Miskonsepsi Umum
- ❌ "Bounded context = microservice." → Bukan. Satu context bisa monolith module. Satu service bisa berisi 3 context (buruk, tapi terjadi).
- ❌ "Satu model entity per tabel, dipakai semua fitur — DRY." → DRY di level code sering merusak DRY di level semantik. Duplikasi model lintas context itu benar.
- ❌ "DDD wajib Aggregate + Repository + Domain Event + Value Object." → DDD tactical patterns hanya berguna bila domain-nya kompleks. CRUD app butuh bounded context? Mungkin. Butuh aggregate? Tidak.
- ❌ "Bounded context harus dipisah DB." → Idealnya, tapi batas logis bisa di-enforce lebih dulu (schema terpisah, larangan join). Pisah DB fizikal belakangan.
- ❌ "Ubiquitous language artinya satu nama untuk semua." → Artinya satu nama per context, dan nama itu dipakai konsisten oleh developer dan domain expert di context itu.
- ❌ "Kalau ada duplikasi data, berarti desainnya salah." → Duplikasi data (denormalisasi lintas context) adalah konsekuensi normal; duplikasi logika invariant (aturan bisnis di dua tempat) itu yang salah.
✅ Cek Pemahaman
-
E-commerce kamu punya
Productdengan 55 kolom: harga, stok, berat, deskripsi SEO, tarif pajak. Tim gudang, marketing, dan finance semuanya edit tabel yang sama dan sering konflik. Berapa bounded context yang kamu usulkan, dan apa konsekuensi yang harus kamu terima (sebutkan minimal 2)? -
Dua service
ordersdaninventoryselalu harus deploy bersamaan dan share satu PostgreSQL. Apakah ini dua bounded context? Apa artinya untuk desain kamu selanjutnya? -
Vendor payment mengirim field
transaction_status: "settled_at_gateway". Tim kamu pakai nilai itu langsung sebagai status order di seluruh app. Apa yang seharusnya ada di antara vendor dan domain kamu, dan kenapa?
🔑 Poin Kunci
- Bounded context = batas makna, bukan batas teknis. Model kontekstual, bukan universal.
- Batas ditemukan lewat domain event + linguistic boundary, diuji lewat coupling perubahan dan kebutuhan transaksi.
- Batas tanpa context map + ACL akan bocor dalam 6 bulan.
- Setiap batas menambah biaya integrasi; hanya ambil bila isolasi perubahan sepadan.
- Monolith modular adalah pilihan sah dan sering pilihan terbaik. Split saat nyeri, bukan saat tegang.
- Distributed monolith = batas ditarik salah. Gejala paling jelas: rilis wajib barengan.
➡️ Eksplorasi Lanjut
- Aggregate design rules (Vaughn Vernon, Effective Aggregate Design) — batas transaksi di dalam context.
- Saga & outbox pattern — cara menjaga konsistensi lintas context tanpa 2PC.
- Modular monolith — batas ditegakkan tanpa jaringan; paling relevan untuk skala tim kamu.
- Event schema evolution — versioning, compatibility mode, consumer tolerance.
- Context map dalam praktik — customer-supplier dengan contract test (Pact) sebagai enforcement.