← semua materi
05:00 Arsitektur · 05 Nov 2026 · 05:01

📚 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


🏗️ 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

  1. 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.

  2. 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.

  3. 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.

  4. 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).

  5. Aggregate terlalu besar — Order memuat seluruh Customer dan semua Product entity → 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


✅ Cek Pemahaman

  1. E-commerce kamu punya Product dengan 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)?

  2. Dua service orders dan inventory selalu harus deploy bersamaan dan share satu PostgreSQL. Apakah ini dua bounded context? Apa artinya untuk desain kamu selanjutnya?

  3. 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


➡️ Eksplorasi Lanjut