Domain-Driven Design (DDD) adalah metodologi yang merekatkan model peranti lunak dengan domain bisnis riil. Dirintis oleh Eric Evans, DDD terbagi menjadi dua bagian: strategic design yang memetakan landscape besar domain menjadi bounded contexts, dan tactical design yang menyusun building block (entity, value object, aggregate, repository) di setiap context. Pada chapter ini kita akan membangun model DDD untuk toko dengan beberapa bounded context (Pengiriman, Penagihan, Inventaris, Penjualan), lalu menerapkan tactical pattern seperti aggregate root Pesanan, repository, dan domain service LayananPengiriman.
Strategic Design: Bounded Context dan Ubiquitous Language #
Strategic design memetakan domain besar menjadi bounded context yang lebih kecil. Setiap context memiliki ubiquitous language: istilah bisnis yang dipakai konsisten antara developer dan domain expert. Istilah yang sama bisa berbeda makna di context berbeda.
| Bounded context | “Alamat” berarti | “Produk” berarti |
|---|---|---|
| Pengiriman | Jalan, kota, kode pos tujuan kirim | SKU, dimensi, berat |
| Penagihan | Pemegang kartu, jalan, kota | Harga jual, deskripsi |
| Inventaris | Lokasi rak | SKU, stok |
| Penjualan | Alamat pelanggan | Harga, kategori |
Menggabungkan semua makna ke dalam satu class Alamat atau Produk menghasilkan model yang membengkak dan rentan konflik. Bounded context memisahkan mereka secara eksplisit.
Tactical Design: Building Blocks #
Lima building block utama DDD:
| Building block | Ciri | Implementasi Kotlin |
|---|---|---|
| Value object | Tanpa identitas, immutable | value class atau data class |
| Entity | Punya identitas, mutable | class dengan id |
| Aggregate root | Entry point ke aggregate, kontrol konsistensi | class dengan field private |
| Repository | Abstraksi akses data aggregate | interface Repository<T, ID> |
| Domain service | Logika yang melibatkan beberapa aggregate | class Layanan... |
Implementasi DDD untuk Toko #
// Demo Domain-Driven Design: strategic (bounded contexts) + tactical (entities, value objects, aggregates, repositories).
import java.time.LocalDate
// === STRATEGIC DESIGN ===
// Bounded Context: Pengiriman
// Di sini, "Alamat" punya atribut jalan, kota, kode pos
data class AlamatPengiriman(val jalan: String, val kota: String, val kodePos: String)
// Bounded Context: Penagihan
// Di sini, "Alamat" punya atribut untuk penagihan (bisa beda dengan pengiriman)
data class AlamatPenagihan(val pemegang: String, val jalan: String, val kota: String)
// Bounded Context: Inventaris
// Di sini, "Produk" punya SKU dan stok; tidak peduli dengan harga jual
data class ProdukInventaris(val sku: String, val stok: Int)
// Bounded Context: Penjualan
// Di sini, "Produk" punya harga jual; tidak peduli dengan stok
data class ProdukPenjualan(val id: Int, val nama: String, val harga: Double)
// === TACTICAL DESIGN ===
// Value Object: immutable, dibandingkan dari nilai
@JvmInline
value class Uang(val nilai: Double) {
operator fun plus(lain: Uang) = Uang(nilai + lain.nilai)
operator fun minus(lain: Uang) = Uang(nilai - lain.nilai)
}
// Entity: mutable, punya identitas (id)
class Pelanggan(val id: Int, var nama: String, var alamatEmail: String) {
override fun equals(other: Any?) = other is Pelanggan && other.id == id
override fun hashCode() = id
}
// Aggregate Root: Pesanan mengontrol konsistensi ItemPesanan
class Pesanan(val id: Int, val pelangganId: Int) {
private val items = mutableListOf<ItemPesanan>()
private var status: StatusPesanan = StatusPesanan.DRAFT
fun tambahItem(produk: ProdukPenjualan, jumlah: Int) {
require(status == StatusPesanan.DRAFT) { "Pesanan sudah tidak dalam status DRAFT" }
require(jumlah > 0) { "Jumlah harus positif" }
items.add(ItemPesanan(produk, jumlah))
}
fun total(): Uang = items.fold(Uang(0.0)) { acc, i -> acc + i.subtotal() }
fun checkout() {
require(items.isNotEmpty()) { "Pesanan kosong" }
status = StatusPesanan.DIPROSES
}
fun ringkas() = "Pesanan #$id pelanggan=$pelangganId ${items.size} item total=${total().nilai} status=$status"
}
data class ItemPesanan(val produk: ProdukPenjualan, val jumlah: Int) {
fun subtotal(): Uang = Uang(produk.harga * jumlah)
}
enum class StatusPesanan { DRAFT, DIPROSES, DIKIRIM, SELESAI }
// Repository: abstraksi akses data
interface PesananRepository {
fun simpan(pesanan: Pesanan)
fun cari(id: Int): Pesanan?
fun semua(): List<Pesanan>
}
class InMemoryPesananRepository : PesananRepository {
private val store = mutableMapOf<Int, Pesanan>()
override fun simpan(pesanan: Pesanan) { store[pesanan.id] = pesanan }
override fun cari(id: Int): Pesanan? = store[id]
override fun semua(): List<Pesanan> = store.values.toList()
}
// Domain Service: logika yang melibatkan beberapa aggregate
class LayananPengiriman {
fun kirim(pesanan: Pesanan, alamat: AlamatPengiriman): String {
require(pesanan.total().nilai > 0) { "Total harus positif" }
return "Pesanan #${pesanan.id} dikirim ke ${alamat.jalan}, ${alamat.kota}"
}
}
// Ubiquitous Language: istilah bisnis yang dipakai konsisten di kode dan diskusi tim
// Pesanan, Pelanggan, ItemPesanan, Checkout, DIPROSES, DIKIRIM, SELESAI
fun main() {
println("=== Demo Domain-Driven Design ===")
// Strategic: bounded context terpisah
val produkInv = ProdukInventaris("BK001", 100)
val produkJual = ProdukPenjualan(1, "Buku Kotlin", 75_000.0)
println("Inventaris: ${produkInv.sku} stok=${produkInv.stok}")
println("Penjualan: ${produkJual.id} harga=${produkJual.harga}")
println()
println("--- Tactical: aggregate root ---")
val pelanggan = Pelanggan(1, "Budi", "[email protected]")
val p = Pesanan(101, pelanggan.id)
p.tambahItem(produkJual, 2)
p.tambahItem(ProdukPenjualan(2, "Pulpen", 5_000.0), 3)
println(p.ringkas())
p.checkout()
println(p.ringkas())
println()
println("--- Tactical: repository ---")
val repo = InMemoryPesananRepository()
repo.simpan(p)
println("Cari pesanan #101: ${repo.cari(101)?.ringkas()}")
println("Total pesanan di repo: ${repo.semua().size}")
println()
println("--- Tactical: domain service ---")
val alamat = AlamatPengiriman("Jl. Merdeka 10", "Jakarta", "10000")
val pengiriman = LayananPengiriman()
println(pengiriman.kirim(p, alamat))
}Output yang Dihasilkan:
=== Demo Domain-Driven Design ===
Inventaris: BK001 stok=100
Penjualan: 1 harga=75000.0
--- Tactical: aggregate root ---
Pesanan #101 pelanggan=1 2 item total=165000.0 status=DRAFT
Pesanan #101 pelanggan=1 2 item total=165000.0 status=DIPROSES
--- Tactical: repository ---
Cari pesanan #101: Pesanan #101 pelanggan=1 2 item total=165000.0 status=DIPROSES
Total pesanan di repo: 1
--- Tactical: domain service ---
Pesanan #101 dikirim ke Jl. Merdeka 10, JakartaBedah Kode Baris-per-Baris #
Mari kita perhatikan bagian kunci:
- Baris 8-22 (bounded contexts): Empat context terpisah dengan model
AlamatdanProdukyang berbeda. Tidak ada satu classProdukserbaguna yang membengkak; setiap context punya model sendiri. - Baris 27-31 (
value class Uang): Value object untuk uang dengan operatorplusdanminus. Perhitungan uang tetap berada dalam tipe yang aman, bukanDoublementah. - Baris 34-38 (
Pelanggan): Entity denganid. MethodequalsdanhashCodedi-override berdasarkanidsaja, karena duaPelanggandenganidsama dianggap entity yang sama meskipunnamaberbeda. - Baris 41-57 (
Pesananaggregate root): Menyembunyikanitemsdanstatussebagai fieldprivate. MethodtambahItemdancheckoutmemeriksa invariant sebelum mengubah state. - Baris 80-85 (
LayananPengiriman): Domain service mengorkestrasi operasi yang melibatkanPesanandanAlamatPengirimandari context berbeda. Logika seperti ini tidak muat di salah satu aggregate.
Penutup #
Domain-Driven Design menggabungkan strategic design (bounded context, ubiquitous language) dengan tactical design (entity, value object, aggregate, repository). Kombinasi ini cocok untuk aplikasi dengan domain kompleks yang akan berkembang jangka panjang. Pada chapter berikutnya kita akan beralih ke Design Patterns: katalog pola GoF dengan implementasi idiomatic Kotlin.
Referensi #
Referensi Lanjutan #
- Kotlin documentation home
— halaman utama dokumentasi Kotlin sebagai rujukan
value classdan sealed hierarchy untuk membangun DDD