Architecture Patterns untuk Menjaga Model Tetap Bersih
Sampai di artikel ini, kita sudah membangun model domain yang mencerminkan cara bisnis bekerja. Tapi muncul tantangan baru: bagaimana cara menjaga model itu tetap bersih ketika sistem kita tumbuh, framework berubah, dan kebutuhan makin kompleks?
Inilah peran architecture patterns, arsitektur perangkat lunak yang membantu memisahkan logika bisnis dari hal-hal teknis, agar domain model tidak tenggelam di tengah “kebisingan” infrastruktur.
Arsitektur Itu Bukan Soal Layer Saja
Kebanyakan engineer mendengar kata “arsitektur” lalu langsung berpikir tentang layered architecture, lapisan presentasi, logika bisnis, dan data. Padahal, arsitektur dalam konteks DDD bukan cuma soal membagi layer, tapi menentukan bagaimana model domain berinteraksi dengan bagian lain dari sistem.
Arsitektur adalah cara menjaga agar domain model tetap berdaulat, tidak dikendalikan oleh framework, database, atau API eksternal.
Dalam DDD, domain model adalah “raja”, dan arsitektur adalah istana yang melindunginya.
Mengapa Butuh Pola Arsitektur?
Tanpa pola arsitektur yang jelas, kode domain akan cepat “tercemar” oleh hal-hal teknis. Awalnya mungkin cuma satu query SQL di dalam entitas, lalu satu logika HTTP handler di service. Lama-lama model domain kehilangan fokus, menjadi campuran antara logika bisnis, UI, dan infrastruktur.
Inilah yang menyebabkan Big Ball of Mud, sistem yang sulit dipahami karena semua bagian saling menempel tanpa batas yang jelas.
Pola arsitektur membantu kita menetapkan batas tanggung jawab (separation of concerns) antara:
Domain logic (apa yang bisnis lakukan),
Application logic (kapan bisnis dijalankan),
dan Infrastructure logic (bagaimana bisnis dijalankan di dunia nyata).
Beberapa Pola Arsitektur Umum
Buku Khononov (2022) membahas beberapa pola arsitektur yang sering digunakan dalam implementasi DDD: Layered Architecture, Ports and Adapters (Hexagonal Architecture), dan CQRS. Mari kita bahas satu per satu dengan contoh sederhana.
1. Layered Architecture, Struktur Paling Umum
Arsitektur berlapis ini mungkin paling dikenal. Biasanya terdiri dari tiga atau empat lapisan:
+------------------------+
| Presentation Layer |
+------------------------+
| Application Layer |
+------------------------+
| Domain Layer |
+------------------------+
| Infrastructure Layer |
+------------------------+
Presentation Layer Menangani interaksi dengan pengguna atau sistem lain (API, UI, CLI).
Application Layer Mengatur flow atau urutan operasi — kapan bisnis dieksekusi.
Domain Layer Tempat Entity, Value Object, dan Domain Service berada.
Infrastructure Layer Berisi detail teknis seperti database, file system, atau network.
Contoh sederhana (Rust):
// Domain Layer
struct Order {
id: u64,
total: f64,
}
impl Order {
fn confirm(&mut self) {
println!("Order {} dikonfirmasi", self.id);
}
}
// Application Layer
fn confirm_order(order_repo: &mut impl OrderRepository, id: u64) {
if let Some(mut order) = order_repo.find_by_id(id) {
order.confirm();
order_repo.save(order);
}
}
// Infrastructure Layer
trait OrderRepository {
fn find_by_id(&self, id: u64) -> Option<Order>;
fn save(&mut self, order: Order);
}
Layered architecture mudah dipahami, cocok untuk banyak aplikasi. Namun, jika tidak dijaga dengan disiplin, lapisan bisa bocor, logika bisnis sering naik ke Application Layer atau turun ke Infrastructure Layer.
2. Ports and Adapters (Hexagonal Architecture)
Kadang disebut juga Hexagonal Architecture, pola ini dikembangkan oleh Alistair Cockburn. Tujuannya: memisahkan domain dari dunia luar lewat port (antarmuka) dan adapter (implementasi teknis).
Bayangkan domain sebagai inti heksagon, dan setiap sisi dihubungkan oleh “port” yang bisa dipasang atau dilepas sesuai kebutuhan: API, database, atau event queue.
+-----------------------+
| Presentation API |
+-----------+-----------+
|
+-------------+-------------+
| Domain Core |
| (Entities, Services, etc) |
+-------------+-------------+
|
+-----------+-----------+
| Infrastructure / DB |
+-----------------------+
Kuncinya: Domain tidak tahu siapa yang memanggilnya, dan tidak peduli siapa yang menyimpan datanya.
Contoh sederhana (Rust):
// Port (interface)
trait PaymentGateway {
fn pay(&self, amount: f64) -> bool;
}
// Domain core
struct PaymentService<'a> {
gateway: &'a dyn PaymentGateway,
}
impl<'a> PaymentService<'a> {
fn process(&self, amount: f64) {
if self.gateway.pay(amount) {
println!("Pembayaran berhasil!");
} else {
println!("Pembayaran gagal.");
}
}
}
// Adapter (implementasi)
struct FakeGateway;
impl PaymentGateway for FakeGateway {
fn pay(&self, _amount: f64) -> bool {
true
}
}
Dengan pola ini, kita bisa mengganti implementasi PaymentGateway tanpa menyentuh kode domain. Mau pakai Midtrans, Stripe, atau simulator lokal, tinggal ganti adapter-nya.
3. CQRS (Command Query Responsibility Segregation)
CQRS memisahkan dua jenis operasi utama sistem:
Command → mengubah data (buat, ubah, hapus).
Query → membaca data.
Tujuannya agar keduanya bisa berevolusi dengan kebutuhan berbeda, misalnya skala, performa, atau keamanan.
Dalam sistem sederhana, Command dan Query bisa digabung. Tapi dalam sistem kompleks, pemisahan ini membuat sistem lebih efisien dan mudah dioptimasi.
Contoh sederhana (Rust):
// Command handler
fn confirm_order(order_repo: &mut impl OrderRepository, id: u64) {
if let Some(mut order) = order_repo.find_by_id(id) {
order.confirm();
order_repo.save(order);
}
}
// Query handler
fn get_order_details(order_repo: &impl OrderRepository, id: u64) -> Option<Order> {
order_repo.find_by_id(id)
}
Kita bisa punya dua pipeline terpisah, satu untuk memproses perubahan, satu untuk membaca data (misalnya dari cache atau database yang dioptimalkan untuk query). CQRS sering dikombinasikan dengan Event Sourcing agar setiap perubahan bisa dilacak dengan mudah.
Memilih Pola yang Tepat
Tidak ada arsitektur yang “paling benar.” Pola yang ideal tergantung dari:
Kompleksitas domain,
Skala sistem,
Kebutuhan integrasi, dan
Tim yang mengelola.
Sebagai panduan umum:
Gunakan Layered Architecture untuk sistem yang masih tumbuh.
Gunakan Hexagonal Architecture jika ingin model domain benar-benar independen dari framework atau infrastruktur.
Gunakan CQRS bila ada kebutuhan pemisahan beban baca/tulis yang signifikan.
Menghindari Arsitektur yang Membelenggu
Sering kali engineer terjebak pada arsitektur yang mereka anggap “suci”, sampai akhirnya domain model malah dikorbankan demi mematuhi struktur. DDD justru sebaliknya: arsitektur harus melayani domain, bukan sebaliknya.
Kuncinya adalah menjaga boundaries tetap jelas, tapi tidak kaku. Ketika model domain berkembang, arsitektur boleh ikut menyesuaikan, selama tujuannya sama: melindungi logika bisnis dari gangguan teknis.
Ringkasnya, Arsitektur Sebagai Penjaga Makna
Arsitektur bukan tentang folder, file, atau diagram. Ia tentang cara melindungi makna bisnis di dalam sistem kita.
Layer, port, dan adapter hanyalah alat untuk memastikan kode kita tetap bercerita tentang domain, bukan tentang framework. Karena pada akhirnya, sistem yang paling bertahan lama bukan yang paling modern arsitekturnya, tapi yang paling setia pada makna bisnis yang dilayaninya.
Referensi
Khononov, V. (2022). Learning Domain-Driven Design: Aligning Software Architecture and Business Strategy. O’Reilly Media.