DOMAIN-DRIVEN-DESIGN DDD SOFTWARE-ARCHITECTURE SYSTEM-DESIGN BACKEND DESIGN-PATTERNS CLEAN-ARCHITECTURE SOFTWARE-DEVELOPMENT MAINTAINABILITY SCALABILITY BUSINESS-LOGIC

Domain-Driven Design (DDD): Membangun Aplikasi Berbasis Bisnis yang Fleksibel dan Tahan Perubahan

⏱️ 17 menit baca
👨‍💻

Domain-Driven Design (DDD): Membangun Aplikasi Berbasis Bisnis yang Fleksibel dan Tahan Perubahan

1. Pendahuluan

Pernahkah Anda merasa kesulitan saat mengembangkan aplikasi yang semakin kompleks? Kode yang tadinya rapi, perlahan menjadi spaghetti. Fitur baru terasa sulit ditambahkan, dan perubahan kecil justru menimbulkan efek domino yang tidak terduga. Seringkali, masalah ini muncul karena kita terlalu fokus pada sisi teknis tanpa benar-benar memahami atau merepresentasikan domain bisnis yang ingin kita pecahkan.

Di sinilah Domain-Driven Design (DDD) hadir sebagai penyelamat. DDD adalah sebuah pendekatan pengembangan perangkat lunak yang menempatkan pemahaman mendalam tentang domain bisnis sebagai inti dari seluruh proses desain dan pengembangan. Tujuannya? Membangun aplikasi yang tidak hanya berfungsi secara teknis, tetapi juga secara akurat mencerminkan logika bisnis, mudah dipahami oleh semua pihak (developer maupun business stakeholder), serta fleksibel dan tahan terhadap perubahan di masa depan.

Dalam artikel ini, kita akan menyelami dunia DDD, memahami konsep-konsep kuncinya, dan melihat bagaimana kita bisa menerapkannya untuk membangun aplikasi yang lebih tangguh dan adaptif. Siapkah Anda membangun aplikasi yang benar-benar berbicara bahasa bisnis? Mari kita mulai!

2. Apa Itu Domain-Driven Design (DDD)?

📌 DDD bukan framework, bukan library, melainkan sebuah filosofi dan seperangkat prinsip.

Domain-Driven Design (DDD) adalah sebuah metodologi pengembangan perangkat lunak yang dipopulerkan oleh Eric Evans dalam bukunya “Domain-Driven Design: Tackling Complexity in the Heart of Software” (2003). Intinya, DDD mengajarkan kita untuk:

  1. Fokus pada Domain Bisnis: Seluruh desain perangkat lunak harus berpusat pada pemahaman yang kaya dan mendalam tentang domain bisnis.
  2. Kolaborasi Erat: Developer dan pakar domain (orang yang memahami bisnis secara mendalam) harus bekerja sama erat untuk membangun model domain yang akurat.
  3. Ubiquitous Language: Menggunakan satu bahasa yang sama (istilah, definisi) antara teknis dan bisnis.
  4. Membangun Model: Membuat model perangkat lunak yang secara langsung merepresentasikan konsep-konsep dalam domain bisnis.

Bayangkan Anda sedang membangun aplikasi untuk sebuah perpustakaan. Tanpa DDD, Anda mungkin langsung memikirkan tabel books, users, transactions. Dengan DDD, Anda akan mulai dengan pertanyaan: Apa itu “buku” bagi perpustakaan ini? Apakah “anggota” sama dengan “pengguna”? Bagaimana proses “peminjaman” bekerja? Pertanyaan-pertanyaan ini akan membentuk model domain Anda.

3. Mengapa DDD Penting? Masalah yang Dipecahkan

Mengapa kita harus repot-repot dengan DDD? Bukankah langsung coding lebih cepat? ✅ Memang, pada proyek kecil, DDD mungkin terasa berlebihan. Namun, untuk aplikasi yang kompleks dan terus berkembang, DDD menawarkan solusi untuk masalah-masalah umum ini:

💡 DDD membantu kita membangun sistem yang “benar” (sesuai kebutuhan bisnis) dan “benar dibangun” (maintainable, scalable).

4. Fondasi DDD: Konsep Strategis

Konsep strategis dalam DDD membantu kita memahami dan memecah domain besar menjadi bagian-bagian yang lebih kecil dan terkelola. Ini adalah langkah awal yang krusial.

a. Bounded Context

Ini adalah salah satu konsep terpenting dalam DDD. Sebuah Bounded Context adalah batasan logis di mana sebuah model domain tertentu didefinisikan dan berlaku. Di luar batasan ini, istilah atau konsep yang sama bisa memiliki arti yang berbeda.

🎯 Analogi: Bayangkan sebuah perusahaan e-commerce. Kata “Produk” bisa berarti berbeda di departemen yang berbeda:

Meskipun semua berbicara tentang “Produk”, model di balik setiap departemen sangat berbeda. Masing-masing departemen ini bisa menjadi Bounded Context yang terpisah. Mencoba menyatukan semua definisi “Produk” dalam satu model akan menghasilkan God Object yang rumit dan sulit diatur.

Manfaat Bounded Context:

b. Ubiquitous Language

Ubiquitous Language adalah bahasa bersama yang digunakan oleh semua anggota tim — developer, business analyst, product owner, bahkan pengguna akhir — untuk mendeskripsikan domain bisnis. Bahasa ini harus konsisten dan eksplisit, baik dalam percakapan, dokumentasi, maupun kode.

Contoh dari e-commerce:

Kesalahan umum: Developer menggunakan terminologi teknis mereka sendiri (DTO, Entity, DAO) sementara bisnis menggunakan terminologi bisnis (Pesanan, Pelanggan, Pengiriman). Ini menyebabkan miskomunikasi.

💡 Ubiquitous Language adalah lem yang menyatukan pemahaman teknis dan bisnis.

c. Context Mapping

Setelah mengidentifikasi Bounded Context yang berbeda, Context Mapping adalah proses mendokumentasikan bagaimana Bounded Context tersebut berinteraksi satu sama lain. Ini membantu kita memahami hubungan, dependensi, dan batasan antara berbagai bagian sistem.

Beberapa pola Context Mapping:

Memahami Context Mapping sangat penting saat mendesain arsitektur microservices, karena membantu menentukan batasan layanan dan bagaimana mereka berkomunikasi.

5. Fondasi DDD: Konsep Taktis

Konsep taktis dalam DDD adalah blok bangunan yang kita gunakan untuk mengimplementasikan model domain di dalam sebuah Bounded Context. Ini adalah cara kita menulis kode.

a. Entitas (Entities)

Entitas adalah objek dalam domain yang memiliki identitas yang unik dan persisten seiring waktu. Identitas ini tetap sama, bahkan jika atributnya berubah.

Contoh dalam sistem e-commerce:

// Contoh Entitas dalam Java
public class Customer {
    private final CustomerId id; // Identitas unik
    private String name;
    private String email;

    public Customer(CustomerId id, String name, String email) {
        this.id = id;
        this.name = name;
        this.email = email;
    }

    public CustomerId getId() { return id; }
    public String getName() { return name; }
    public String getEmail() { return email; }

    public void changeEmail(String newEmail) {
        // Validasi dan logika bisnis terkait perubahan email
        if (newEmail == null || !newEmail.contains("@")) {
            throw new IllegalArgumentException("Invalid email format");
        }
        this.email = newEmail;
    }

    // hashCode() dan equals() berdasarkan id
}

// Contoh Value Object untuk ID
public record CustomerId(String value) {}

Karakteristik Entitas:

b. Value Objects

Value Object adalah objek yang mendeskripsikan karakteristik atau atribut domain tanpa memiliki identitas unik. Mereka didefinisikan oleh nilai-nilainya. Jika semua nilainya sama, maka dua Value Object dianggap sama. Mereka bersifat immutable (tidak dapat diubah setelah dibuat).

Contoh:

// Contoh Value Object dalam Java
public record Money(BigDecimal amount, String currency) {
    public Money {
        if (amount.compareTo(BigDecimal.ZERO) < 0) {
            throw new IllegalArgumentException("Amount cannot be negative");
        }
        if (currency == null || currency.isBlank()) {
            throw new IllegalArgumentException("Currency cannot be empty");
        }
    }

    public Money add(Money other) {
        if (!this.currency.equals(other.currency)) {
            throw new IllegalArgumentException("Cannot add money with different currencies");
        }
        return new Money(this.amount.add(other.amount), this.currency);
    }
    // Tidak ada setter, immutable
}

Manfaat Value Object:

c. Aggregate

Aggregate adalah kluster Entitas dan Value Object yang diperlakukan sebagai satu unit untuk tujuan perubahan data. Setiap Aggregate memiliki satu Root Entitas (juga disebut Aggregate Root) yang menjadi satu-satunya titik akses eksternal ke Aggregate tersebut. Semua operasi pada Aggregate harus melalui Aggregate Root.

🎯 Tujuan Aggregate: Menjaga konsistensi data dan domain invariant (aturan bisnis yang harus selalu benar) di dalam batas Aggregate.

Contoh: Pesanan (Order) sebagai Aggregate Root. Sebuah Pesanan mungkin terdiri dari:

Ketika Anda mengubah OrderItem (misalnya, menambahkan produk), Anda melakukannya melalui objek Order. Order bertanggung jawab untuk memastikan bahwa perubahan ini tidak melanggar aturan bisnis (misalnya, stok tersedia, total harga diperbarui dengan benar).

public class Order { // Aggregate Root
    private final OrderId id;
    private CustomerId customerId; // Hanya ID, bukan seluruh objek Customer
    private List<OrderItem> items;
    private Money totalAmount;
    private OrderStatus status; // Enum

    public Order(OrderId id, CustomerId customerId, List<OrderItem> items) {
        this.id = id;
        this.customerId = customerId;
        this.items = new ArrayList<>(items);
        this.status = OrderStatus.PENDING;
        calculateTotalAmount();
    }

    public void addOrderItem(ProductId productId, int quantity, Money itemPrice) {
        // Logika bisnis untuk menambahkan item, validasi stok, dll.
        OrderItem newItem = new OrderItem(OrderItemId.generate(), productId, quantity, itemPrice);
        this.items.add(newItem);
        calculateTotalAmount();
    }

    public void confirm() {
        if (this.status != OrderStatus.PENDING) {
            throw new IllegalStateException("Order cannot be confirmed from status " + this.status);
        }
        // Logika bisnis untuk konfirmasi order (misal: kurangi stok, buat pembayaran)
        this.status = OrderStatus.CONFIRMED;
    }

    private void calculateTotalAmount() {
        // Logika untuk menghitung total dari semua item
        // ...
    }

    // Getters
}

public class OrderItem { // Entitas di dalam Aggregate Order
    private final OrderItemId id;
    private ProductId productId; // Hanya ID
    private int quantity;
    private Money price;

    // Constructor, getters
}

⚠️ Aturan Penting Aggregate:

d. Domain Services

Domain Services adalah operasi domain yang tidak secara alami masuk ke dalam Entitas atau Value Object. Mereka biasanya melibatkan beberapa Aggregate atau objek domain lain dan melakukan koordinasi.

Contoh:

public class OrderPlacementService {
    private final OrderRepository orderRepository;
    private final ProductRepository productRepository;
    private final PaymentGateway paymentGateway;

    public OrderPlacementService(OrderRepository orderRepository, ProductRepository productRepository, PaymentGateway paymentGateway) {
        this.orderRepository = orderRepository;
        this.productRepository = productRepository;
        this.paymentGateway = paymentGateway;
    }

    public Order placeOrder(CustomerId customerId, List<OrderItemRequest> itemRequests) {
        // 1. Buat Order Aggregate
        Order newOrder = new Order(OrderId.generate(), customerId, itemRequests.stream().map(req -> {
            // Logika untuk membuat OrderItem dari request
            // ...
        }).toList());

        // 2. Validasi dan kurangi stok produk (melalui ProductRepository)
        // ...

        // 3. Proses pembayaran (melalui PaymentGateway)
        // ...

        // 4. Simpan Order
        orderRepository.save(newOrder);

        return newOrder;
    }
}

Karakteristik Domain Services:

e. Repositories

Repository adalah objek yang menyediakan antarmuka untuk menyimpan dan memuat Aggregate dari dan ke persistent storage (misalnya, database). Repository menyembunyikan detail implementasi database dari domain model.

Contoh:

public interface OrderRepository {
    Optional<Order> findById(OrderId id);
    void save(Order order);
    // ... metode lain untuk query atau penghapusan Aggregate Order
}

// Contoh implementasi (misalnya dengan JPA/Hibernate)
public class JpaOrderRepository implements OrderRepository {
    // ... injeksi EntityManager atau Spring Data JPA Repository
    @Override
    public Optional<Order> findById(OrderId id) {
        // Logika untuk mengambil Order dari DB
        // ...
    }

    @Override
    public void save(Order order) {
        // Logika untuk menyimpan Order ke DB
        // ...
    }
}

Manfaat Repositories:

6. Menerapkan DDD dalam Proyek Anda: Sebuah Contoh Sederhana

Mari kita bayangkan skenario sederhana: Anda ingin menambahkan fitur “Pembatalan Pesanan” ke sistem e-commerce.

Tanpa DDD (Pendekatan Anemic/Prosedural): Anda mungkin memiliki OrderService yang langsung memanipulasi entitas Order dan OrderItem secara langsung, mengubah status di database.

public class OrderService {
    public void cancelOrder(String orderId) {
        Order order = orderDao.findById(orderId);
        if (order.getStatus().equals("COMPLETED")) {
            throw new IllegalStateException("Cannot cancel completed order.");
        }
        order.setStatus("CANCELLED");
        orderDao.update(order);
        // Mungkin ada logika untuk mengembalikan stok, refund, dll.
        // Semua logika ini tersebar di service layer.
    }
}

Masalahnya: Logika bisnis untuk pembatalan pesanan ("COMPLETED" tidak boleh dibatalkan, refund harus diproses) tersebar di OrderService, bukan di Order itu sendiri. Jika ada OrderCancellationService lain, logika ini bisa diduplikasi atau terlewat.

Dengan DDD: Logika pembatalan menjadi tanggung jawab Order Aggregate Root.

public enum OrderStatus {
    PENDING, CONFIRMED, SHIPPED, COMPLETED, CANCELLED
}

public class Order { // Aggregate Root
    private final OrderId id;
    private OrderStatus status;
    private List<OrderItem> items; // ... detail lain

    // Constructor...

    public void cancel() {
        if (this.status == OrderStatus.COMPLETED || this.status == OrderStatus.SHIPPED) {
            throw new DomainException("Order cannot be cancelled if already shipped or completed.");
        }
        // Logika bisnis kompleks terkait pembatalan, misal:
        // - Mengembalikan stok produk (mungkin melalui Domain Event)
        // - Menandai untuk refund (mungkin melalui Domain Event)
        this.status = OrderStatus.CANCELLED;
        // Setelah ini, Aggregate Root dianggap 'dirty' dan akan disimpan oleh Repository.
    }

    public OrderStatus getStatus() {
        return status;
    }
    // ... getters
}

// Di Application Service (yang mengorkestrasi interaksi dengan Aggregate)
public class OrderApplicationService {
    private final OrderRepository orderRepository;
    // ... dependencies lain seperti EventPublisher, RefundService

    public OrderApplicationService(OrderRepository orderRepository) {
        this.orderRepository = orderRepository;
    }

    public void cancelOrder(OrderId orderId) {
        Order order = orderRepository.findById(orderId)
                          .orElseThrow(() -> new OrderNotFoundException(orderId));

        order.cancel(); // Panggil metode domain pada Aggregate Root

        orderRepository.save(order); // Simpan perubahan Aggregate
        // Mungkin terbitkan Domain Event seperti OrderCancelledEvent
        // eventPublisher.publish(new OrderCancelledEvent(orderId));
    }
}

Dalam pendekatan DDD, Order Aggregate bertanggung jawab penuh atas konsistensi internalnya. Metode cancel() ada di dalam Order itu sendiri, memastikan bahwa aturan bisnis (seperti tidak bisa membatalkan pesanan yang sudah SHIPPED atau COMPLETED) selalu ditegakkan. OrderApplicationService hanya berperan sebagai orkestrator, memuat Aggregate, memanggil operasi domain, dan menyimpan kembali Aggregate. Ini membuat kode lebih bersih, lebih mudah dipahami, dan lebih tahan terhadap perubahan.

Kesimpulan

Domain-Driven Design mungkin terdengar kompleks pada awalnya, tetapi ini adalah investasi berharga untuk aplikasi yang kompleks dan berumur panjang. Dengan berfokus pada domain bisnis, menggunakan Ubiquitous Language yang konsisten, dan memecah sistem menjadi Bounded Context yang terkelola, kita dapat membangun model perangkat lunak yang secara akurat mencerminkan realitas bisnis.

Konsep-konsep taktis seperti Entities, Value Objects, Aggregates, Domain Services, dan Repositories adalah alat yang ampuh untuk mengimplementasikan model domain yang bersih, fleksibel, dan teruji. Mulailah dengan mengidentifikasi Bounded Context dan Ubiquitous Language di proyek Anda, lalu perlahan terapkan konsep taktisnya. Anda akan menemukan bahwa membangun aplikasi yang tadinya terasa seperti labirin, kini menjadi lebih terstruktur dan menyenangkan.

Selamat mendesain dengan DDD!

🔗 Baca Juga