DDD

DDD (Domain Driven Development) adalah sebuah filosofi pengembangan aplikasi yang berpusat pada logic bisnis yang disebut sebagai “Domain”. Filosofi ini pertama kali diperkenalkan oleh Eric Evans di tahun 2003. Walaupun udah lebih dari 20 tahun yang lalu tapi filosofi ini masih tetap relevan di jaman sekarang. DDD menyelaraskan antara code dengan realita bisnis, bukan sebaliknya. Fokus saat develop aplikasi adalah bisnisnya, bukan teknologinya. DDD memiliki beberapa komponen yang harus diperhatikan seperti Ubiquitous Language, Bounded Context, Context Mapping, Entity Object, Value Object, Aggregate Object, Domain Service, dan Repository.
Ubiquitous Language
Ubiquitous Language artinya adalah sebuah istilah yang dipakai di mana-mana. Istilah yang dipakai di dalam bisnis harus seragam dengan istilah yang digunakan pada code. Misalnya pada dunia marketing ada istilah “Canvassing” untuk penjualan barang secara langsung, maka pada code kita juga harus menggunakan istilah ini seperti pada objek “SalesCanvassing”, bukan “DoorToDoorSales”. Contoh lainnya adalah “Invoice” untuk istilah faktur pembelian. Jika tim di lapangan menggunakan istilah itu maka di dalam code kita juga harus menggunakan istilah yang sama, bukan menggunakan istilah lain seperti “Billing”. Meskipun keduanya bermakna sama, tapi apa yang develop harus selaras dengan istilah yang sering digunakan di lapangan. Berlaku juga untuk beberapa istilah lain yang bermakna mirip seperti “Product” dengan “Item”, “Sales” dengan “Revenue”, “Delivery” dengan “Shipment”, atau isitlah lainnya.
Bounded Context
Bounded Context adalah batasan istilah yang disepakati. Meskipun kita telah menyepakati sebuah istilah yang akan digunakan, kadang istilah yang sama digunakan untuk beberapa konteks berbeda. Di sini kita harus batasi penggunaan istilahnya sesuai konteks. Misalnya istilah “Item”, di dalam konteks penjualan “Item” maknanya adalah barang yang dijual. Di dalam konteks pergudangan “Item” adalah barang yang disimpan. Di dalam konteks pengiriman “Item” adalah barang yang dikirim. Batasan konteks ini harus jelas. Artinya akan ada 3 objek “Item” untuk tiap konteks seperti “SalesItem” untuk barang yang dijual, “StockItem” untuk barang yang disimpan, dan “DeliveryItem” untuk barang yang dikirim. Jangan sampai hanya karena istilahnya sama kita hanya bikin satu objek doang yang dipakai untuk semua konteks. Kita harus memahami kapan konteks tersebut berbeda. Misalnya pada konteks penjualan “SalesItem” mengandung informasi kode barang, jumlah barang yang dipesan, dan jumlah barang yang dikonfirmasi. Sedangkan pada konteks pergudangan “StockItem” mengandung informasi kode barang, jumlah barang yang tersedia, jumlah barang yang dipesan, dan jumlah barang yang diretur.
Context Mapping
Context Mapping adalah strategi untuk memetakan konteks tentang bagaimana konteks tersebut berkomunikasi. Ada beberapa strategi Context Mapping:
Customer-Supplier
Ini sama kayak analogi Customer-Supplier, yaitu Customer bergantung pada Supplier. Customer beli barang milik Supplier. Tanpa Supplier, Customer ga akan bisa mendapatkan barang karena barang yang dijual sudah pasti lewat Supplier. Pada konteks ini kita bisa membuat strukturnya seperti ini:
public record Item(String sku, int quantity){
}
public record Supplier(String name, List<Item> items){
}
public record Customer(String name, Supplier supplier){
}Pada contoh di atas artinya Item yang ada di Supplier terhubung dengan Customer lewat komposisi. Strategi ini berlaku jika hubungannya dalam bentuk komposisi.
Anti-Corruption Layer
Strategi ini terjadi ketika terjadi perbedaan istilah dengan sistem luar. Ini yang bikin pusing kepala karena istilah yang biasa kita pakai bentrok saat integrasi dengan sistem luar. Misalnya kita menggunakan istilah “Sku” sebagai kode unik barang. Aplikasi kita terintegrasi dengan API pihak luar yang menggunakan istilah “ProductCode” sebagai kode unik barang. Untuk mengatasinya dengan strategi ini adalah kita perlu bikin logic mapping istilah “ProductCode” ini dengan “Sku” sebelum diproses lebih lanjut di dalam code kita. Jadi istilah “ProductCode” ini hanya terisolasi di satu tempat di bagian integrasinya aja, sedangkan di codebase kita secara umum masih menggunakan istilah “Sku”. Ini salah satu strategi yang umum pada project yang sering berintegrasi dengan sistem luar.
Conformist
Ini kebalikan dari Anti-Corruption Layer. Sesuai namanya, di sini kita pasrah saat terjadi perbedaan istilah dengan sistem luar. Biasanya ini dilakukan ketika istilah tersebut ga banyak digunakan di sistem kita. Misalkan kita bikin aplikasi trading saham, tapi di dalamnya ada fitur beli kuota internet yang terintegrasi dengan API luar. Di dalam code kita menggunakan istilah “Credit”, sedangkan pada API integrasi untuk pembelian pulsa menggunakan istilah “Quota”. Daripada repot-repot bikin logic mapping mending pasrah aja dengan risiko istilah di codebase kita ada yang ga konsisten karena itu bikin fitur utama kita dan itu hanya dipake di satu tempat doang.
Shared Kernel
Strategi ini adalah dengan cara menggunakan objek yang sama pada konteks berbeda. Ini bertolak-belakang dengan Bounded Context di mana kita harus memisahkannya tiap konteks. Tapi memang ada kalanya ini bisa dipakai karena memang propertinya memang mirip. Contohnya “Currency” dan “Country” yang sama-sama memiliki properti “Language”. Walaupun konteks beda, tapi keduanya memiliki properti “Language” yang mirip.
public record Language(String name, String code){
}
public record Country(String name, Language language){
}
public record Currency(String name, Language language){
}Tapi yang kayak gini jarang terjadi dan ga bisa dijadiin strategi default karena bisa bikin pusing kalau ternyata strategi ini kurang tepat.
Entity Object
Entity Object adalah objek yang memiliki identitas unik. Sebuah Entity yang memiliki identitas unik sama meskipun data lainnya berubah tetaplah objek yang sama. Di Java kita bisa menggunakan override method equals() dan hashcode() dan tulis logic untuk properti yang dijadikan Id. Kalau pakai Lombok kita bisa menggunakan anotasi @EqualsAndHashCode dan menulis properti yang dijadikan Id. Contohnya pada Entity User dengan username sebagai Id:
@Getter
@AllArgsConstructor
@EqualsAndHashCode(of = "username")
public static class UserEntity{
String username;
String password;
LocalDate birthDate;
}Dua objek User dengan username yang sama adalah objek yang sama.
Value Object
Value Object adalah objek untuk menyimpan data yang immutable dan ga harus punya identitas unik. Sebuah Value yang memiliki data yang sama dengan objek Value lainnya adalah objek yang sama. Di Java kita bisa menggunakan override method equals() dan hashcode() dan tulis logic untuk semua property. Kalau pakai Lombok kita bisa menggunakan anotasi @Value, nanti otomatis dibuat implementasinya menggunakan semua properti. Atau kalau pakai Java versi modern, kita bisa menggunakan Record. Contohnya pada Value yang menyimpan alamat user.
public record UserAddress(String street, String city, String country){
}Karena objeknya immutable, maka saat objeknya berubah kita harus bikin objek baru.
Aggregate Object
Aggregate Object adalah Entity Object atau Value Object yang berisi kumpulan Entity Object atau Value Object lainnya dalam satu kesatuan data. Misalnya User sebagai Aggregate Object dengan value UserAddresses.
public record User(String username, UserAddress address){
}Tanpa objek User, UserAddresses ga akan ada artinya.
Domain Service
Domain Service adalah objek inti yang berisi logic bisnis. Bedanya dengan Entity, Value, dan Aggregate Object adalah di sini ga cuma nyimpan data, tapi juga ada logicnya. Misalnya perubahan state, validasi bisnis, atau logic bisnis lainnya. Contohnya pada objek Domain Password yang menyimpan value password beserta validasi bisnisnya:
public record Password(String value) {
public Password {
if (value == null || value.isBlank()) {
throw new WeakPasswordException("Password must not be blank");
}
if (value.length() < 8) {
throw new WeakPasswordException("Password must be at least 8 characters");
}
if (!value.matches(".*[a-zA-Z].*") || !value.matches(".*\\d.*")) {
throw new WeakPasswordException("Password must contain at least one letter and one number");
}
}
}Konsep ini menentang penggunaan Anemic Data Model yang sering digunakan. Anemic Data Model adalah objek yang logic bisnisnya dipisah di objek berbeda dan isinya cuma ada getter/setter. Contohnya kayak gini:
public record Password(String value) {
}public class PasswordValidator {
public void validate(Password password) {
String value = password.value();
if (value == null || value.isBlank()) {
throw new WeakPasswordException("Password must not be blank");
}
if (value.length() < 8) {
throw new WeakPasswordException("Password must be at least 8 characters");
}
if (!value.matches(".*[a-zA-Z].*") || !value.matches(".*\\d.*")) {
throw new WeakPasswordException("Password must contain at least one letter and one number");
}
}
}Objek Password disebut kayak kurang darah. Pendekatan seperti itu ditentang oleh filosofi ini dan dinilai anti-pattern OOP. Manipulasi data dilakukan di luar objek dan dianggap lebih berorientasi prosedural daripada berorientasi objek. Domain Service ini mengangkat marwah objek inti.
Repository
Repository adalah interface untuk mekanisme penyimpanan dan pengambilan data ke database. Objek-objek sebelumnya tugasnya menyimpan data ke objek, nah yang ini tugasnya menyimpan data ke database.
public interface UserRepository {
void save(UserEntity user);
boolean existsByUsername(String username);
Optional<UserEntity> findByUsername(String username);
}Verdict
DDD mendorong kita untuk berpikir tentang aturan bisnis terlebih dulu dibanding berpikir teknis. Logic bisnis ditempatkan sebagai pusat utama. Setelah itu baru diterjemahkan ke dalam teknis. DDD cocok digunakan pada project besar yang sangat kompleks dan maintanability tinggi. Keuntungan terbesarnya adalah code benar-benar merepresentasikan bisnis, bukan teknologi.
