Foto oleh Dmitry Romanoff via Pexels

Setelah memahami konsep dasar Clean Architecture, langkah selanjutnya adalah memahami secara mendalam tanggung jawab spesifik masing-masing layer: Domain, Application, dan Infrastructure, serta bagaimana ketiganya berinteraksi tanpa melanggar Dependency Rule yang sudah dibahas sebelumnya.

Domain berisi entity dan aturan bisnis paling fundamental yang jarang berubah, Application berisi use case yang mengorkestrasi entity Domain untuk mencapai tujuan bisnis tertentu, dan Infrastructure berisi implementasi teknis konkret seperti akses database dan pemanggilan API eksternal.

Artikel ini membahas secara detail apa yang seharusnya ditempatkan di masing-masing layer, dengan contoh konkret struktur project yang memisahkan ketiganya dengan jelas dalam konteks aplikasi ASP.NET Core.

Domain: Entity dan Business Rules Murni

Entity Domain membungkus data sekaligus aturan bisnisnya sendiri

namespace TokoOnline.Domain.Entities; public class Pesanan { public int Id { get; private set; } public decimal Total { get; private set; } public StatusPesanan Status { get; private set; } public void Batalkan() { if (Status == StatusPesanan.Selesai) throw new InvalidOperationException("Pesanan selesai tidak bisa dibatalkan"); Status = StatusPesanan.Dibatalkan; } }
Entity Pesanan tidak sekadar menjadi wadah data pasif, melainkan juga membungkus aturan bisnis yang berkaitan langsung dengannya, seperti method Batalkan yang memvalidasi apakah pesanan masih boleh dibatalkan, memastikan aturan ini tidak bisa dilanggar dari luar entity itu sendiri.

Application: Use Case yang Mengorkestrasi Domain

Use case mengoordinasikan alur, logika bisnis tetap di Domain

namespace TokoOnline.Application.UseCases; public class BatalkanPesananUseCase { private readonly IPesananRepository _repository; public BatalkanPesananUseCase(IPesananRepository repository) => _repository = repository; public async Task ExecuteAsync(int pesananId) { var pesanan = await _repository.GetByIdAsync(pesananId); pesanan.Batalkan(); // logika bisnis ada di Domain await _repository.SaveAsync(pesanan); } }
Layer Application berisi use case yang mengatur alur kerja (seperti mengambil data, memanggil method bisnis pada entity, lalu menyimpan perubahan), namun logika bisnis sesungguhnya tetap berada di entity Domain, menjaga Application tetap tipis dan fokus pada orkestrasi, bukan tempat menumpuk aturan bisnis.

Infrastructure: Implementasi Teknis Konkret

Infrastructure mengimplementasikan interface dengan teknologi konkret

namespace TokoOnline.Infrastructure.Repositories; public class PesananRepository : IPesananRepository { private readonly TokoDbContext _context; public PesananRepository(TokoDbContext context) => _context = context; public async Task<Pesanan> GetByIdAsync(int id) => await _context.Pesanan.FindAsync(id); public async Task SaveAsync(Pesanan pesanan) => await _context.SaveChangesAsync(); }
PesananRepository di layer Infrastructure mengimplementasikan interface IPesananRepository yang didefinisikan di Domain atau Application, memakai Entity Framework Core sebagai teknologi konkret untuk benar-benar berinteraksi dengan database, menjadi satu-satunya tempat yang perlu diubah jika teknologi database diganti di kemudian hari.

Struktur Project yang Mencerminkan Pemisahan Layer

Struktur project fisik mencerminkan Dependency Rule

TokoOnline.sln ├── TokoOnline.Domain/ (tidak referensi project lain) ├── TokoOnline.Application/ (referensi Domain saja) ├── TokoOnline.Infrastructure/ (referensi Domain dan Application) └── TokoOnline.Api/ (referensi semua, titik komposisi)
Memecah solution menjadi project csproj terpisah untuk setiap layer memberikan jaminan fisik, bukan hanya konvensi, bahwa Domain tidak bisa mereferensikan Infrastructure karena memang tidak ada reference project yang mengizinkannya, membuat pelanggaran Dependency Rule terdeteksi sejak tahap kompilasi.

Contoh Konkret Memisahkan Entity Domain dari Model Infrastructure

Entity domain dengan behavior vs model infrastruktur yang hanya menyimpan kolom

// Domain: entity murni, tidak tahu soal Entity Framework public class Produk { public int Id { get; private set; } public string Nama { get; private set; } public decimal Harga { get; private set; } public void UbahHarga(decimal hargaBaru) { if (hargaBaru <= 0) throw new ArgumentException("Harga harus positif"); Harga = hargaBaru; } } // Infrastructure: model EF Core terpisah, dipetakan ke tabel public class ProdukEntity { public int Id { get; set; } public string Nama { get; set; } public decimal Harga { get; set; } }

Pemisahan layer domain dari infrastructure sering kali paling terasa pada bagaimana entity dimodelkan. Entity domain seperti Produk di atas membungkus datanya dengan properti private set dan hanya mengizinkan perubahan lewat method seperti UbahHarga, yang bisa menegakkan aturan bisnis seperti harga tidak boleh negatif.

Model infrastructure seperti ProdukEntity sebaliknya dibuat sesederhana mungkin, murni merepresentasikan struktur tabel agar mudah dipetakan oleh Entity Framework atau ORM lain. Pemetaan antara kedua model ini biasanya dilakukan di layer Infrastructure lewat repository, sehingga layer Domain dan Application tidak pernah bersentuhan langsung dengan atribut spesifik ORM seperti navigation property atau foreign key.

Pendekatan dua model ini menambah sedikit kode mapping dibanding memakai satu entity untuk semua keperluan, namun hasilnya domain tetap bersih dari ketergantungan pada framework data access. Ketika suatu saat tabel database perlu dinormalisasi ulang atau ORM diganti, perubahan cukup dilakukan di layer Infrastructure tanpa menyentuh aturan bisnis yang sudah teruji di layer Domain.

Mapping antara Domain Entity dan DTO di Lapisan Application

DTO menyembunyikan detail entity domain dan menambahkan format tampilan

// DTO yang dikembalikan ke client, terpisah dari entity domain public class ProdukDto { public int Id { get; set; } public string Nama { get; set; } public string HargaFormatted { get; set; } // "Rp 45.000" } // Mapping manual di Application layer, bukan di Controller public class ProdukMapper { public static ProdukDto KeDto(Produk produk) => new ProdukDto { Id = produk.Id, Nama = produk.Nama, HargaFormatted = produk.Harga.ToString("C0", new CultureInfo("id-ID")) }; }

Mengembalikan entity domain langsung sebagai response API tampak praktis di awal, namun lama-kelamaan membocorkan detail internal seperti method bisnis atau field yang seharusnya tidak pernah dilihat client, serta membuat perubahan pada entity domain langsung berdampak pada kontrak API. DTO (Data Transfer Object) berfungsi sebagai lapisan pemisah yang hanya memuat data yang memang perlu dikirim ke luar, dengan bentuk yang bisa berbeda dari struktur entity domain aslinya. Perubahan skema database pun tidak langsung memaksa perubahan pada kontrak API, selama DTO yang diekspos ke client tetap dijaga stabil.

Mapping antara entity dan DTO idealnya dilakukan di lapisan Application, bukan di Controller atau di dalam entity domain itu sendiri, karena Application adalah lapisan yang memang bertanggung jawab mengoordinasikan data untuk kebutuhan presentasi. Mapping manual seperti pada contoh memberi kontrol penuh atas transformasi data, seperti memformat harga menjadi string mata uang, yang tidak perlu dan tidak boleh menjadi tanggung jawab entity domain. Struktur DTO juga bisa disesuaikan khusus untuk tiap use case, misalnya versi ringkas untuk daftar dan versi lengkap untuk halaman detail produk.

Untuk project dengan banyak entity dan DTO, menulis mapping manual untuk setiap kombinasi bisa menjadi repetitif, sehingga library seperti AutoMapper sering dipakai untuk menghasilkan mapping otomatis berdasarkan kecocokan nama properti. Trade-off-nya, mapping otomatis membuat transformasi data menjadi kurang eksplisit dan lebih sulit ditelusuri saat debugging dibanding mapping manual yang terlihat jelas langkah demi langkah di dalam kode. Validasi konfigurasi mapping otomatis tetap diperlukan lewat unit test, agar perubahan nama properti pada entity tidak diam-diam membuat field DTO menjadi kosong.

Domain Event untuk Memberi Tahu Perubahan Penting di Layer Domain

Entity domain mencatat event tanpa mengetahui siapa yang akan menanganinya

// Entity domain menghasilkan event saat kondisi bisnis penting terjadi public class Pesanan { private readonly List<object> _events = new(); public void Konfirmasi() { Status = StatusPesanan.Dikonfirmasi; _events.Add(new PesananDikonfirmasiEvent(Id)); } public IReadOnlyList<object> AmbilEvents() => _events; } // Setelah disimpan, Application layer membaca event lalu memicu aksi lanjutan // seperti mengirim notifikasi, tanpa Pesanan perlu tahu soal notifikasi

Domain event adalah catatan bahwa sesuatu yang penting telah terjadi di dalam entity domain, seperti pesanan yang baru saja dikonfirmasi. Entity cukup menghasilkan event tersebut tanpa perlu tahu siapa saja yang akan bereaksi terhadapnya, sehingga logika domain tetap bersih dari urusan seperti pengiriman notifikasi atau pembaruan laporan.

Setelah entity disimpan, lapisan Application membaca daftar event yang dihasilkan lalu memicu handler terpisah untuk masing-masing event, misalnya satu handler mengirim email dan handler lain memperbarui data statistik. Pola ini memudahkan penambahan reaksi baru terhadap kejadian bisnis yang sama tanpa perlu mengubah kode di dalam entity Pesanan itu sendiri.

Kesalahan Umum yang Sering Terjadi

Menaruh Logika Validasi Bisnis di Use Case, Bukan di Entity

Menulis validasi seperti 'pesanan selesai tidak bisa dibatalkan' di dalam use case Application alih-alih di method entity Domain membuat aturan bisnis tersebar dan berpotensi dilanggar jika ada jalur kode lain yang mengubah status pesanan tanpa melalui use case tersebut.

Membuat Use Case yang Terlalu Generic dan Menangani Banyak Hal

Use case yang menangani terlalu banyak skenario sekaligus dalam satu class kehilangan kejelasan single responsibility yang menjadi salah satu manfaat utama memisahkan use case per aksi bisnis yang spesifik.

Tidak Memisahkan Project Secara Fisik Sesuai Layer

Hanya memisahkan layer dengan folder dalam satu project csproj yang sama tanpa reference project terpisah tidak memberikan jaminan fisik terhadap Dependency Rule, hanya mengandalkan disiplin developer yang bisa saja terlewat tanpa disadari.

Kapan Pendekatan Ini Tidak Cocok Dipakai

Memisahkan project secara fisik per layer memberikan jaminan arsitektural yang kuat dan terdeteksi sejak kompilasi, namun menambah kompleksitas setup solution dengan banyak project kecil yang saling mereferensikan.

Untuk project yang lebih kecil, memisahkan layer hanya dengan folder dalam satu project bisa menjadi kompromi yang lebih ringan, meski kehilangan jaminan fisik terhadap Dependency Rule dan sepenuhnya bergantung pada disiplin tim dalam menjaga pemisahan tersebut.

Ringkasan

Memisahkan Domain, Application, dan Infrastructure dengan jelas memastikan logika bisnis inti tetap murni dan tidak tercampur dengan detail teknis, dengan use case di Application yang hanya mengorkestrasi tanpa menyimpan aturan bisnis itu sendiri. Struktur project terpisah secara fisik memberikan jaminan arsitektural yang lebih kuat dibanding sekadar pemisahan folder.

Seri Belajar REST API & Clean Architecture:

← Sebelumnya: Pengenalan Clean Architecture pada Aplikasi Backend

→ Selanjutnya: Dependency Rule dalam Clean Architecture