Domain-Driven Design: Mengatur Struktur Codebase Besar

Architecture By TryzTech Team
Domain-Driven DesignDDDSoftware ArchitectureSoftware EngineeringClean Code
Bagikan

Daftar Isi

Apa Itu Domain-Driven Design?

Domain-Driven Design (DDD) adalah pendekatan desain software yang menempatkan domain bisnis sebagai pusat keputusan teknis. Fokusnya bukan sekadar membagi folder menjadi controller, service, dan repository, tetapi memahami aturan bisnis, istilah yang dipakai user, serta batas tanggung jawab antar bagian sistem.

Dalam codebase kecil, struktur sederhana sering cukup. Namun saat produk tumbuh, logika bisnis mulai tersebar: validasi ada di controller, aturan pricing ada di service, perubahan status ada di model, dan istilah antar tim mulai tidak konsisten. DDD membantu mengembalikan struktur kode ke bahasa dan batas bisnis yang lebih jelas.

DDD bukan pengganti clean code atau SOLID principles. Justru, DDD biasanya lebih efektif ketika fondasi kode sudah cukup rapi: nama jelas, dependency terkendali, dan tanggung jawab modul tidak saling bocor.

Masalah yang Diselesaikan DDD

DDD berguna ketika kompleksitas utama aplikasi bukan lagi framework, database, atau deployment, melainkan aturan bisnis.

Beberapa tanda codebase mulai membutuhkan pendekatan seperti DDD:

  • istilah bisnis berbeda antara product, support, dan engineering
  • satu fitur kecil menyentuh terlalu banyak file
  • logika bisnis penting tersebar di banyak layer
  • model data database terlalu mendikte desain kode
  • tim sulit menjelaskan “bagian sistem ini milik siapa”
  • perubahan aturan bisnis sering menimbulkan bug regresi

Jika masalah utamanya masih CRUD sederhana, DDD penuh mungkin terlalu berat. Tetapi beberapa konsepnya, seperti ubiquitous language dan bounded context, tetap bisa dipakai tanpa mengubah seluruh arsitektur.

DDD juga membantu mengelola technical debt karena ia memberi cara untuk memetakan area kode berdasarkan domain bisnis, bukan hanya berdasarkan folder teknis. Saat boundary domain jelas, prioritas refactor dan ownership jadi lebih mudah dibicarakan.

Ubiquitous Language

Konsep paling penting dalam DDD adalah ubiquitous language, yaitu bahasa bersama yang dipakai oleh domain expert, product, designer, support, dan engineer.

Misalnya dalam sistem e-commerce, istilah seperti “cart”, “order”, “invoice”, “shipment”, dan “refund” tidak boleh dipakai secara asal. Jika product menyebut “order” sebagai transaksi yang sudah dibayar, tetapi engineer memakai “order” untuk keranjang yang belum checkout, bug komunikasi hampir pasti muncul.

Ubiquitous language harus tercermin di:

  • nama class dan function
  • nama event
  • nama command
  • dokumentasi API
  • test case
  • percakapan tim

Contoh buruk:

function processData(input) {
  // ...
}

Contoh lebih baik:

function approveRefundRequest(request: RefundRequest) {
  // ...
}

Nama kedua langsung membawa konteks bisnis. Ini sejalan dengan kebiasaan naming yang baik dalam clean code: nama bukan dekorasi, melainkan alat komunikasi.

Building Block Penting dalam DDD

DDD punya banyak konsep, tetapi beberapa building block berikut paling sering dipakai dalam codebase aplikasi.

Entity

Entity adalah object yang identitasnya penting dan bisa berubah seiring waktu.

Contoh:

class Customer {
  constructor(
    public readonly id: CustomerId,
    public email: EmailAddress,
    public status: CustomerStatus
  ) {}

  suspend(reason: string) {
    if (this.status === 'closed') {
      throw new Error('Closed customer cannot be suspended');
    }

    this.status = 'suspended';
  }
}

Customer tetap Customer yang sama meskipun email atau statusnya berubah, karena identitasnya ada di CustomerId.

Value Object

Value object tidak ditentukan oleh ID, tetapi oleh nilainya.

Contoh:

class Money {
  constructor(
    public readonly amount: number,
    public readonly currency: string
  ) {
    if (amount < 0) {
      throw new Error('Amount cannot be negative');
    }
  }
}

Money(100000, 'IDR') sama dengan Money(100000, 'IDR') lain karena nilainya sama. Value object cocok untuk menyimpan validasi kecil yang sering tersebar di banyak tempat.

Aggregate

Aggregate adalah kumpulan entity dan value object yang diperlakukan sebagai satu unit konsistensi.

Misalnya Order bisa menjadi aggregate root yang mengatur OrderItem, total harga, status pembayaran, dan aturan pembatalan. Kode luar tidak boleh mengubah OrderItem sembarangan tanpa melewati aturan Order.

class Order {
  private items: OrderItem[] = [];

  addItem(productId: ProductId, quantity: number, price: Money) {
    if (this.status !== 'draft') {
      throw new Error('Only draft orders can be changed');
    }

    this.items.push(new OrderItem(productId, quantity, price));
  }
}

Aggregate membantu menjaga aturan bisnis tetap berada di satu tempat. Ini juga membuat review perubahan lebih mudah, karena reviewer bisa fokus pada boundary domain, bukan hanya syntax. Untuk budaya review yang sehat, lihat juga code review feedback.

Repository

Repository adalah abstraksi untuk mengambil dan menyimpan aggregate.

interface OrderRepository {
  findById(id: OrderId): Promise<Order | null>;
  save(order: Order): Promise<void>;
}

Repository bukan tempat menaruh semua query. Ia sebaiknya mewakili kebutuhan domain. Untuk query baca yang sangat kompleks, pola seperti CQRS bisa dipertimbangkan agar model tulis dan baca tidak saling memaksa.

Domain Service

Domain service dipakai saat aturan bisnis tidak cocok dimiliki oleh satu entity atau value object tertentu.

Contoh: menghitung eligibility customer untuk promosi mungkin membutuhkan Customer, Order History, dan Campaign Rule sekaligus. Jika logika itu dipaksa masuk ke salah satu entity, tanggung jawabnya bisa melebar.

Gunakan domain service untuk aturan domain, bukan untuk semua operasi aplikasi. Jika semua hal dinamai SomethingService, biasanya desain domain belum cukup jelas.

Bounded Context

Bounded context adalah batas tempat sebuah model dan bahasa berlaku.

Satu istilah bisa punya arti berbeda di bagian sistem yang berbeda. Contoh:

  • Dalam konteks Sales, “Customer” adalah prospek atau pembeli.
  • Dalam konteks Billing, “Customer” adalah pihak yang punya invoice dan metode pembayaran.
  • Dalam konteks Support, “Customer” adalah orang yang punya tiket bantuan.

Memaksa satu model Customer untuk semua konteks sering membuat class membengkak. Dengan bounded context, setiap area boleh punya model sendiri selama integrasi antar konteks jelas.

Bounded context juga penting saat sistem mulai mengarah ke microservices architecture. Microservice yang baik biasanya lahir dari boundary bisnis yang jelas, bukan dari memecah service berdasarkan tabel database.

Kapan DDD Cocok Digunakan?

DDD cocok ketika:

  • domain bisnis kompleks dan terus berubah
  • tim sering salah paham istilah bisnis
  • banyak aturan penting tersebar di layer aplikasi
  • produk punya beberapa area bisnis yang berbeda
  • codebase mulai sulit dipahami berdasarkan struktur teknis saja
  • biaya bug domain cukup tinggi

DDD mungkin terlalu berat ketika:

  • aplikasi masih CRUD sederhana
  • tim kecil sedang mengejar validasi MVP
  • domain belum stabil
  • masalah utama ada di performa, bukan desain domain
  • belum ada test atau proses review yang cukup matang

Tidak perlu menerapkan DDD secara ekstrem dari hari pertama. Mulai dari bahasa domain, value object untuk aturan kecil, dan boundary folder yang lebih jelas. Setelah itu, baru pertimbangkan aggregate, repository, dan bounded context yang lebih formal.

Contoh Struktur Folder Sederhana

Berikut contoh struktur sederhana untuk modul order:

src/
  modules/
    order/
      domain/
        Order.ts
        OrderItem.ts
        Money.ts
        OrderRepository.ts
        OrderPolicy.ts
      application/
        CreateOrder.ts
        CancelOrder.ts
        PayOrder.ts
      infrastructure/
        PostgresOrderRepository.ts
        OrderHttpController.ts
      tests/
        Order.test.ts
        CreateOrder.test.ts

Penjelasan singkat:

  • domain/ berisi aturan bisnis inti.
  • application/ berisi use case atau alur aplikasi.
  • infrastructure/ berisi detail framework, database, HTTP, queue, atau vendor.
  • tests/ menjaga perilaku domain dan use case tetap aman saat berubah.

Struktur ini bukan aturan mutlak. Yang penting adalah dependency mengarah ke domain, bukan sebaliknya. Domain sebaiknya tidak bergantung pada framework web atau ORM tertentu.

Kesalahan Umum Saat Menerapkan DDD

1. Terlalu cepat membuat abstraksi

Banyak tim langsung membuat folder domain, application, dan infrastructure untuk semua fitur, bahkan yang masih sederhana. Hasilnya bukan desain yang bersih, melainkan ceremony tambahan.

Mulai dari area yang memang punya kompleksitas bisnis nyata.

2. Menganggap DDD sama dengan struktur folder

DDD bukan sekadar folder. Jika nama class masih generik, aturan bisnis masih tersebar, dan product-engineering masih memakai istilah berbeda, folder rapi tidak banyak membantu.

3. Repository menjadi query dump

Repository yang berisi semua jenis query akan berubah menjadi god object baru. Pisahkan kebutuhan domain dari kebutuhan reporting atau read model.

4. Melupakan test

DDD membuat aturan bisnis lebih eksplisit. Tanpa test, manfaat ini tidak maksimal karena tim tetap takut mengubah domain logic.

5. Mengabaikan komunikasi

DDD sangat bergantung pada percakapan. Engineer perlu aktif bertanya ke domain expert, bukan hanya menerjemahkan tiket menjadi kode.

FAQ

Apakah DDD wajib untuk semua project?

Tidak. DDD paling berguna untuk aplikasi dengan domain bisnis kompleks. Untuk CRUD sederhana, sebagian konsepnya bisa dipakai secara ringan tanpa menerapkan semuanya.

Apakah DDD harus memakai microservices?

Tidak. DDD bisa diterapkan di monolith. Bahkan, banyak tim sebaiknya mulai dari modular monolith dulu sebelum memecah sistem menjadi microservices.

Apa hubungan DDD dengan Clean Architecture?

DDD fokus pada modeling domain bisnis. Clean Architecture fokus pada arah dependency dan pemisahan layer. Keduanya bisa saling melengkapi, tetapi bukan hal yang sama.

Apakah entity DDD sama dengan tabel database?

Tidak selalu. Entity dalam DDD mewakili konsep domain yang punya identitas. Tabel database adalah detail penyimpanan. Kadang bentuknya mirip, kadang berbeda.

Dari mana mulai menerapkan DDD?

Mulai dari satu area yang paling sering berubah atau paling sering menimbulkan bug. Rapikan istilah, tulis aturan domain sebagai kode yang eksplisit, tambahkan test, lalu perbaiki boundary secara bertahap.

Kesimpulan

Domain-Driven Design membantu tim membangun codebase yang mengikuti bentuk bisnis, bukan hanya bentuk database atau framework. Dengan ubiquitous language, bounded context, entity, value object, aggregate, repository, dan domain service, tim punya alat untuk mengelola kompleksitas yang muncul saat produk tumbuh.

Namun DDD bukan obat ajaib. Ia bekerja paling baik ketika dipakai secara pragmatis: mulai dari area yang benar-benar kompleks, jaga bahasa tetap konsisten, dan kombinasikan dengan praktik engineering seperti clean code, SOLID, testing, dan code review yang sehat.

Kalau kamu pernah menerapkan DDD di codebase besar, atau justru merasa konsep ini terlalu berat untuk timmu, tulis pengalamanmu di komentar. Perspektif dari proyek nyata biasanya jauh lebih membantu daripada teori yang terlalu rapi.

Lanjutkan membaca topik yang masih satu konteks.

Jangan Ketinggalan Info Terbaru

Dapatkan artikel teknologi, tips, dan insights menarik langsung ke email Kamu.