Pola Logika Bisnis Kompleks dengan Model Domain
Kalau di artikel-artikel sebelumnya kita bicara tentang cara membatasi konteks (bounded context) dan mengintegrasikannya, sekarang kita masuk ke jantung dari Domain-Driven Design (DDD): Domain Model.
Bagian ini menjawab pertanyaan besar: “Bagaimana cara membuat kode yang benar-benar mencerminkan cara bisnis bekerja, bukan sekadar menyimpan data?”
Dari Strategi ke Taktik
Dalam DDD, ada dua lapisan pendekatan: Strategic DDD dan Tactical DDD.
Strategic DDD membantu kita memetakan peta besar, membagi sistem menjadi domain, subdomain, dan bounded context.
Sementara Tactical DDD berbicara tentang bagaimana kita membangun isi di dalam satu bounded context itu.
Kalau Strategic DDD seperti membuat denah kota, maka Tactical DDD adalah merancang bangunan di dalam tiap distrik: siapa penghuni, bagaimana alurnya, dan aturan apa yang berlaku di dalamnya.
Domain Model: Jantung dari DDD
Sebuah domain model bukan sekadar diagram UML atau tabel database.
Ia adalah representasi konseptual dari cara bisnis bekerja, diterjemahkan ke dalam kode, menggunakan bahasa yang sama dengan yang digunakan oleh para ahli domain.
Model ini bukan hanya soal data structure, tapi juga soal perilaku (behavior) dan aturan (rules) yang melekat di dalamnya.
Contoh sederhana:
Bayangkan kita membuat sistem untuk toko online. Ada entitas Order, tapi Order bukan cuma tabel dengan kolom id, total, dan status.
Ia harus tahu bagaimana:
menambahkan item,
menghitung total,
memastikan tidak ada duplikasi produk,
dan kapan pesanan bisa dikonfirmasi.
Model yang baik berarti kode kita bicara seperti bisnisnya.
Entity
Salah satu elemen paling penting dari domain model adalah Entity, sebuah objek yang punya identitas unik dan bisa berubah seiring waktu.
Misalnya, User dengan ID 123 tetap sama orangnya meski alamat atau emailnya berubah.
Yang menentukan dirinya adalah identitas, bukan nilainya.
struct User {
id: u64,
name: String,
email: String,
}
impl User {
fn change_email(&mut self, new_email: String) {
self.email = new_email;
}
}Di sini User adalah Entity karena kita mengenalinya lewat id.
Ketika change_email() dipanggil, objek ini masih “orang yang sama”.
Value Object
Berbeda dengan Entity, Value Object tidak punya identitas.
Ia hanya penting karena nilainya. Dua objek yang nilainya sama dianggap sama.
Coba pikirkan Money, Address, atau DateRange.
Kalau kamu punya Money { amount: 100, currency: “USD” }, itu identik dengan objek lain yang punya nilai sama.
#[derive(Debug, Clone, PartialEq, Eq)]
struct Money {
amount: f64,
currency: String,
}Value Object biasanya immutable, artinya nilainya tidak diubah, tapi dibuat baru saat ada perubahan.
Pendekatan ini membuat kode lebih aman dan mudah diprediksi.
Aggregate
Saat bisnis mulai kompleks, satu entitas tidak lagi cukup.
Kita butuh sekumpulan entitas dan value object yang bekerja bersama dalam satu batas konsistensi.
Inilah yang disebut Aggregate.
Aggregate adalah kumpulan objek yang dikelola sebagai satu kesatuan logis, dengan satu titik masuk utama yang disebut Aggregate Root.
struct OrderItem {
product_id: u64,
quantity: u32,
}
struct Order {
id: u64,
items: Vec<OrderItem>,
}
impl Order {
fn add_item(&mut self, product_id: u64, quantity: u32) {
self.items.push(OrderItem { product_id, quantity });
}
fn total_items(&self) -> usize {
self.items.len()
}
}Order di atas adalah Aggregate Root — semua perubahan terhadap OrderItem harus melalui Order.
Aggregate membantu menjaga aturan bisnis tetap konsisten di dalam batasnya.
Repository
Domain Model tidak seharusnya tahu tentang database atau API.
Tapi tentu kita tetap perlu menyimpan data.
Untuk itu, DDD memperkenalkan Repository.
Repository berfungsi sebagai “pintu keluar masuk” Aggregate.
Dari sisi domain, Repository terlihat seperti koleksi di memori, meski di baliknya mungkin ada SQL, file, atau service eksternal.
trait OrderRepository {
fn save(&self, order: &Order);
fn find_by_id(&self, id: u64) -> Option<Order>;
}Dengan pola ini, domain tetap bersih dari detail teknis.
Kita bisa mengganti penyimpanan dari SQLite ke PostgreSQL tanpa mengubah logika bisnis sedikit pun.
Domain Service
Kadang kita menemukan logika bisnis yang tidak cocok dimasukkan ke satu entitas mana pun.
Misalnya, proses transfer uang antar akun: melibatkan dua Account, tapi tidak ada satu pun yang benar-benar “memiliki” logika itu.
Inilah peran Domain Service, tempat untuk menaruh logika lintas entitas.
struct Account {
id: u64,
balance: f64,
}
struct TransferService;
impl TransferService {
fn transfer(from: &mut Account, to: &mut Account, amount: f64) {
if from.balance >= amount {
from.balance -= amount;
to.balance += amount;
}
}
}Domain Service tetap bagian dari domain layer, bukan service teknis seperti API handler atau queue worker.
Ia merepresentasikan aksi bisnis, bukan infrastruktur.
Factory
Membuat entitas kadang tidak sesederhana memanggil new().
Ada validasi, aturan bisnis, atau dependensi lain.
Di sinilah Factory digunakan.
Factory bertugas membangun objek kompleks dengan aturan bisnis yang benar sejak awal.
struct OrderFactory;
impl OrderFactory {
fn create_new(items: Vec<OrderItem>) -> Order {
assert!(!items.is_empty(), “Order harus punya minimal satu item”);
Order { id: 0, items }
}
}Factory mencegah kita membuat objek domain dalam keadaan “tidak valid”.
Ia seperti dapur yang memastikan masakan sudah matang sebelum dihidangkan.
Domain Event
Dalam kehidupan nyata, setiap sistem bisnis penuh dengan peristiwa: pesanan dibuat, pembayaran diterima, stok habis, dsb.
DDD merepresentasikan semua itu lewat Domain Event.
Event adalah sinyal bahwa sesuatu yang penting sudah terjadi di dalam domain.
Biasanya diberi nama dengan bentuk lampau: OrderCreated, PaymentReceived.
struct OrderCreated {
order_id: u64,
timestamp: u64,
}
Event bisa dikirim ke bagian lain dari sistem (misalnya ke modul pengiriman atau notifikasi) tanpa membuat coupling langsung antar komponen.
Hasilnya: sistem lebih fleksibel dan modular.
Menghadapi Kompleksitas
Setiap sistem yang tumbuh pasti akan menjadi kompleks.
Tactical DDD memberi kita alat untuk mengelola kompleksitas itu, bukan menyingkirkannya.
Entity dan Value Object membantu kita berpikir dalam istilah bisnis, bukan tabel.
Aggregate menjaga aturan tetap konsisten.
Repository dan Factory memisahkan logika domain dari teknis.
Domain Service dan Event membuat sistem bisa berkembang tanpa kekacauan.
Inilah inti filosofi DDD: “Kode bukan cuma alat, tapi cerminan dari cara kita memahami bisnis.”
Ketika model domain dibuat dengan benar, engineer tidak hanya menulis kode, mereka mengkodekan pengetahuan bisnis yang bisa bertahan bertahun-tahun, bahkan ketika framework atau teknologi berubah.
Referensi
Khononov, V. (2022). Learning Domain-Driven Design: Aligning Software Architecture and Business Strategy. O’Reilly Media.

