Jawaban singkat untuk pertanyaan yang paling sering diajukan pembaca tentang topik ini.
01Apa itu bounded context dalam domain-driven design?
Bounded context adalah batas di mana satu domain model berlaku dan setiap istilah dalam ubiquitous language hanya punya satu makna. Di luar batas itu, kata yang sama bisa berarti lain, misalnya Item di sales versus inventory. DDD memakai context alih-alih satu model terpadu karena satu model untuk sistem besar jarang layak atau hemat biaya.
02Bagaimana cara menemukan bounded context di sistem yang sudah ada?
Cari empat sinyal yang cenderung sejalan: kosakata yang berbeda antar tim, kelompok yang muncul dari event storming, kepemilikan tim yang jelas, dan kejelasan siapa yang boleh mengubah data apa. Bahasa dan kepemilikan adalah yang terkuat, karena batas yang memotong kosakata satu tim mahal untuk diperbaiki. Tidak ada proses mekanis, jadi siapkan diri merevisi peta.
03Apa beda bounded context dengan microservice?
Bounded context adalah batas pemodelan, sedangkan microservice adalah unit deployment. Satu context per service sering ideal, tetapi satu context bisa mencakup beberapa service, atau beberapa context berbagi satu service demi kesederhanaan operasional. Mulailah dengan module di dalam monolith dan naikkan hanya bila kepemilikan, scaling atau ritme rilis menuntutnya.
04Apa itu anti-corruption layer dan kapan harus dipakai?
Anti-corruption layer adalah lapisan translasi di tepi context downstream yang mengubah model upstream menjadi kosakatanya sendiri, sehingga konsep asing atau legacy tidak bocor masuk. Pakai bila dua context punya semantik berbeda tetapi harus berkomunikasi, misalnya sistem order legacy. Batasi pada translasi saja dan hindari menaruh business rule di dalamnya.
05Bolehkah dua bounded context berbagi satu tabel database?
Bisa, tetapi itu pada dasarnya Shared Kernel yang tidak direncanakan, dan menciptakan kembali satu model yang justru ingin dihindari bounded context. Setiap perubahan skema lalu butuh kedua tim. Lebih baik tiap context memiliki tabelnya sendiri dan bertukar event berversi atau API eksplisit.
Bounded Context di Domain-Driven Design dan Cara Menemukannya
Apa itu bounded context, mengapa Item punya tiga makna di sales, inventory dan accounting ERP, serta cara menemukan batasnya dan menyambungkannya lewat context map.
Bounded context adalah batas di mana satu domain model dan satu ubiquitous language tetap konsisten, sehingga kata seperti Item boleh berarti berbeda di sales, inventory dan accounting. Temukan batasnya dari perbedaan kosakata, event storming, serta kepemilikan tim dan data, lalu hubungkan antar context dengan context map.
Buka hampir semua skema ERP dan Anda akan menemukan tabel bernama items. Namanya terlihat paling aman di seluruh sistem, tetapi justru sumber perdebatan paling sering: sales ingin harga di dalamnya, gudang ingin lokasi bin, dan accounting ingin akun persediaan. Semuanya benar, dan tidak ada satu class pun yang bisa memuaskan ketiganya tanpa menambah kolom untuk tiap tim.
Tulisan ini menjelaskan bounded context, gagasan Domain-Driven Design yang mengakhiri perdebatan itu. Rujukannya adalah catatan BoundedContext dari Martin Fowler, panduan domain analysis di Azure Architecture Center, dan pola context map Eric Evans seperti diringkas di Wikipedia. Contoh ERP-nya hanyalah ilustrasi dengan angka bulat rekaan, bukan studi kasus hasil pengukuran.
Apa itu bounded context dalam domain-driven design?
Bounded context menentukan batas dalam sebuah domain di mana satu domain model tertentu berlaku. Di dalam batas itu setiap istilah punya satu makna dan modelnya konsisten secara internal; di luarnya, istilah yang sama bisa berarti lain. Fowler menjelaskan motivasinya secara langsung: DDD menangani model besar dengan membaginya menjadi bounded context dan bersikap eksplisit soal hubungan di antaranya.
Gagasan ini menolak satu model terpadu. Fowler mengutip Evans bahwa unifikasi total domain model untuk sistem besar tidak akan layak atau hemat biaya. Alih-alih satu class Item untuk semua orang, tiap context mendapat model kecilnya sendiri dengan atribut yang dibutuhkan saja. Panduan Azure memakai contoh drone: perbaikan dan predictive analysis butuh jarak tempuh dan riwayat maintenance, sedangkan scheduling hanya perlu tahu apakah drone tersedia.
Mengapa kata yang sama berarti berbeda di tiap context?
Karena tiap tim memakai kata itu untuk pekerjaan yang berbeda. Bahasa yang dipakai tim bersama domain expert disebut ubiquitous language, dan panduan Azure mencatat bahwa tiap bounded context boleh punya bahasa sendiri, itulah sebabnya kata seperti account berbeda antar context. Fowler menyebut Customer dan Product sebagai polyseme yang terus berulang. Di ERP, Item adalah contoh paling jelas.
Context
Arti Item di sini
Yang tidak perlu diketahuinya
Sales
Barang yang bisa dijual dengan harga list, dijual per box
Lokasi bin, nomor lot, akun ledger
Inventory
Barang yang bisa distok dan dihitung dalam pieces, dengan reorder point dan bin
Harga pelanggan, diskon, kode pajak
Accounting
Aset bernilai dengan akun persediaan dan standard cost
Di rak mana barang disimpan, bagaimana ditawarkan
Catalogue vs billing
Pembelahan serupa untuk Product: penawaran yang dideskripsikan versus baris yang ditagih
Tiap sisi mengabaikan atribut sisi lain
Perhatikan bahwa ketiga model itu tidak saling bertentangan. Mereka menggambarkan satu benda fisik dari tiga sudut. Masalah baru muncul ketika seseorang memaksa semuanya masuk satu tabel, karena perubahan yang dibutuhkan accounting, misalnya metode valuasi baru, harus dinegosiasikan juga dengan sales dan gudang. Bila Anda merancang sisi stok secara mendalam, tulisan tentang desain modul ERP inventory management dan desain double-entry general ledger membahas masing-masing modelnya.
Bagaimana cara menemukan batas antar bounded context?
Tidak ada proses mekanis yang menghasilkan jawaban benar; panduan Azure mengatakannya dengan jelas. Yang ada adalah empat sinyal yang cenderung sejalan ketika sebuah batas memang nyata.
Perbedaan bahasa. Ketika orang mulai memberi awalan pada satu kata, seperti sales item atau stock item, atau satu kata butuh definisi berbeda di tiap rapat, kosakatanya sudah terbelah.
Event storming. Dalam workshop ini, stakeholder dan developer menata domain event di dinding sebagai sticky note oranye, dan kelompok yang berbagi kosakata serta aggregate menunjuk calon context. Wikipedia menyebut Alberto Brandolini sebagai penemunya.
Tim dan kepemilikan. Fowler berpendapat faktor dominan biasanya budaya manusia, karena model adalah ubiquitous language dan Anda butuh model berbeda ketika bahasanya berubah. Panduan Azure menambahkan bahwa context yang butuh banyak tim, atau satu tim yang memiliki context tak berkaitan, adalah tanda untuk meninjau ulang batasnya.
Kepemilikan data. Tanyakan siapa yang boleh mengubah suatu fakta. Jika hanya gudang yang boleh mengubah stok on-hand, stok tinggal di inventory, dan yang lain memegang salinan atau referensi.
Bila sinyal-sinyal itu tidak sepakat, percayai bahasa dan kepemilikan terlebih dahulu. Kepemilikan data biasanya paling mudah diperbaiki belakangan; batas yang memotong kosakata satu tim adalah jenis yang mahal untuk diperbaiki.
Tes awal yang murah: tulis semua nama kolom yang dipakai dua tim untuk objek nyata yang sama. Jika berbeda, atau nama yang sama membawa aturan berbeda, Anda sudah menemukan calon batas sebelum menggambar satu kotak pun.
Bagaimana bounded context saling berhubungan di context map?
Context map mendokumentasikan hubungan antar context dan menyorot titik integrasi, seperti dijelaskan panduan Azure. Pola hubungan dari Evans, yang diringkas di Wikipedia dan panduan Azure, adalah kosakata untuk menggambarnya. Context upstream menyediakan data atau layanan, dan context downstream bergantung padanya.
Pola
Siapa yang menyesuaikan
Pilih bila
Shared Kernel
Kedua tim, bersama-sama
Dua tim sepakat berbagi subset model yang kecil dan eksplisit, lalu menjaganya tetap kecil
Customer-Supplier
Upstream menyuplai, kontrak dinegosiasikan kedua tim
Tim downstream punya pengaruh nyata pada roadmap upstream
Conformist
Downstream menerima model upstream apa adanya
Antarmuka khusus kecil kemungkinannya, jadi mengikuti justru menyederhanakan integrasi
Anti-Corruption Layer
Downstream menerjemahkan di tepinya sendiri
Model upstream bersifat legacy atau asing dan tidak boleh bocor ke model Anda
Open Host Service plus Published Language
Upstream menerbitkan API dan format yang stabil
Banyak konsumen downstream butuh kontrak terdefinisi yang sama
Pola keenam, Separate Ways, berarti dua context tidak terintegrasi sama sekali dan masing-masing berkembang sendiri. Halaman anti-corruption layer di panduan Azure menambahkan biayanya: latensi ekstra, satu komponen lagi untuk dijalankan, dan aturan agar logika translasi bebas dari business rule dan orkestrasi. Di ERP, bentuk yang umum adalah inventory sebagai open host yang menerbitkan event, accounting sebagai downstream dengan anti-corruption layer, dan payment gateway pihak ketiga sebagai context yang cukup Anda ikuti.
Seperti apa satu konsep di dua context, dalam kode?
Bentuk yang dituju adalah tiga model kecil tanpa class Item bersama, dan fungsi translasi eksplisit di tiap sambungan. Sketsa di bawah memakai angka ilustrasi: SKU yang dijual dalam box berisi 12 pieces berarti lima box menjadi 5 kali 12, yaitu 60 pieces, di batas context.
// The same real-world thing, modelled three times. No shared "Item" class.
// sales/domain/sellable-item.ts - what a customer can buy
export interface SellableItem {
sku: string;
displayName: string;
listPriceIdr: number; // price per BOX
unitsPerBox: number; // sales quotes in boxes
}
// inventory/domain/stock-item.ts - what a warehouse can count
export interface StockItem {
sku: string;
onHandPieces: number; // stock is counted in PIECES
reorderPointPieces: number;
binLocation: string;
lotTracked: boolean;
}
// accounting/domain/ledger-item.ts - what the books must value
export interface LedgerItem {
itemCode: string; // same value as sku, different name on purpose
inventoryAccount: string; // e.g. "1-1300"
cogsAccount: string; // e.g. "5-1000"
standardCostIdrPerPiece: number;
}
// sales/acl/to-reservation.ts - the translation function.
// Sales speaks boxes; inventory speaks pieces. Convert at the boundary,
// never inside either model.
export interface SalesOrderLine { sku: string; quantityBoxes: number }
export interface ReserveStockCommand { sku: string; quantityPieces: number }
export function toReservation(
line: SalesOrderLine,
item: SellableItem,
): ReserveStockCommand {
if (!Number.isInteger(line.quantityBoxes) || line.quantityBoxes <= 0) {
throw new Error("quantityBoxes must be a positive integer");
}
return {
sku: line.sku,
quantityPieces: line.quantityBoxes * item.unitsPerBox,
};
}
// Worked example: 5 boxes of a 12-piece SKU
// 5 x 12 = 60 pieces reserved in inventory.
Dua detail lebih penting daripada sintaksnya. Model accounting menamai identifier itemCode sedangkan yang lain memakai sku, dan itu disengaja: tiap context menamai benda dalam bahasanya sendiri dan translasi yang memegang pemetaannya. Lalu konversi satuan tinggal di satu fungsi di tepi, sehingga tidak ada model yang perlu tahu bahwa yang lain menghitung dengan cara berbeda.
Bagaimana context saling bicara tanpa berbagi database?
Lewat event yang diterbitkan atau API eksplisit, tidak pernah lewat tabel milik context lain. Artikel DDD di Wikipedia membedakan domain event, yang tinggal di dalam satu context dan berisi payload ringan, dari integration event, yang melintasi context dan butuh payload lebih lengkap serta stabil karena pendengarnya beragam. Di bawah, inventory menerbitkan integration event yang diberi versi dan accounting menerjemahkannya menjadi jurnal yang seimbang.
// Published by inventory (the upstream). This is its published language:
// small, versioned, and free of inventory internals such as binLocation.
export interface GoodsIssuedV1 {
type: "inventory.goods-issued.v1";
eventId: string; // idempotency key for consumers
occurredAt: string; // ISO 8601, UTC
sku: string;
quantityPieces: number;
}
// accounting/acl/on-goods-issued.ts - the downstream's anti-corruption layer.
// Accounting never imports inventory types; it translates the event into
// its own vocabulary: a balanced journal entry.
export function toJournalEntry(e: GoodsIssuedV1, item: LedgerItem) {
const amount = e.quantityPieces * item.standardCostIdrPerPiece;
return {
sourceEventId: e.eventId, // unique constraint here makes redelivery safe
lines: [
{ account: item.cogsAccount, debit: amount, credit: 0 },
{ account: item.inventoryAccount, debit: 0, credit: amount },
],
};
}
// Worked example: 60 pieces at a standard cost of 8,500 IDR per piece
// 60 x 8,500 = 510,000 IDR debited to COGS and credited to inventory.
// Debits (510,000) equal credits (510,000), so the entry balances.
Aritmetikanya adalah pengecekannya: 60 pieces dengan standard cost 8.500 IDR menjadi 510.000 IDR, didebit ke harga pokok penjualan dan dikredit ke persediaan, sehingga debit sama dengan kredit. eventId berfungsi juga sebagai idempotency key. Unique constraint pada sourceEventId di accounting membuat event yang terkirim ulang tidak bisa memposting entri yang sama dua kali, dan itulah yang membuat pengiriman asynchronous cukup aman untuk diandalkan.
Tabel yang ditulis oleh dua context adalah Shared Kernel yang tidak pernah Anda sepakati. Kelihatannya jalan pintas, padahal ia menciptakan kembali satu model terpadu: setiap perubahan skema kini butuh kedua tim, dan tak ada yang tahu context mana yang boleh mengubah apa.
Haruskah bounded context menjadi module atau service?
Gagasan ini tidak menyiratkan salah satunya. Artikel DDD di Wikipedia mencatat bahwa satu context per microservice biasanya ideal untuk batas yang jelas dan deployment independen, tetapi pemetaan satu-ke-banyak dan banyak-ke-satu bisa tepat, dengan menimbang prinsip DDD terhadap beban operasional. Di satu VPS, default yang lebih murah adalah menjadikan tiap context sebuah module dengan folder, tabel dan antarmuka publiknya sendiri, seperti dibahas panduan modular monolith architecture, serta mengisolasi port-nya sebagaimana hexagonal architecture. Pakai checklist ini sebelum menaikkan context menjadi service.
Apakah satu tim memiliki context itu dari ujung ke ujung, dan bisa merilis tanpa meminta tim lain?
Apakah ia punya kebutuhan scaling, availability atau ritme rilis yang berbeda dari tetangganya?
Bisakah pemanggilnya hidup dengan kegagalan jaringan dan eventual consistency, bukan pemanggilan lokal dan satu transaksi?
Apakah biaya operasional satu deployable tambahan, lengkap dengan monitoring dan proses rilisnya sendiri, layak dibayar sekarang?
Jika jawabannya kebanyakan tidak, biarkan ia menjadi module. Batasnya adalah bagian yang berharga, dan module yang digambar dengan baik bisa diekstrak nanti karena tepinya sudah eksplisit. Sebaliknya tidak berlaku: service yang dipotong di garis yang salah mengubah setiap perubahan menjadi perubahan terdistribusi.
Di mana posisi aggregate di dalam bounded context?
Aggregate adalah lapisan taktis di dalam sebuah context. Panduan Azure memisahkan strategic DDD, yang menentukan struktur skala besar lewat context, dari tactical DDD, yang menyediakan entity, aggregate dan domain service untuk membangun model di dalam satu context. Aggregate adalah sekumpulan objek yang diperlakukan sebagai satu unit konsistensi dan diubah lewat satu root.
Itu sebabnya batas context datang lebih dulu. Anda tidak bisa memilih aggregate yang masuk akal untuk Item sebelum tahu Item mana yang sedang dimodelkan: StockItem dengan kuantitas on-hand dan bin di inventory, LedgerItem dengan akun di accounting. Transaksi tinggal di dalam satu aggregate, dan apa pun yang melintasi aggregate atau context berjalan sebagai event.
Perlakukan bounded context sebagai janji bahwa satu kata berarti satu hal di dalamnya. Temukan batasnya dari kosakata, kepemilikan dan event yang dijelaskan orang, beri tiap context model kecilnya sendiri, dan terjemahkan di sambungan dengan fungsi yang bisa diuji. Pilih module dulu dan service hanya bila checklist berkata demikian.