Foto oleh David Yu via Pexels

Clean Architecture adalah pola arsitektur perangkat lunak yang dipopulerkan Robert C. Martin (Uncle Bob), bertujuan memisahkan logika bisnis inti aplikasi dari detail teknis seperti database, framework, atau UI, sehingga logika bisnis tersebut bisa diuji dan dipertahankan independen dari perubahan teknologi di sekitarnya.

Prinsip utama Clean Architecture adalah Dependency Rule: dependensi kode hanya boleh mengarah ke dalam, menuju layer yang lebih abstrak (domain/business logic), tidak pernah ke arah luar menuju layer yang lebih konkret seperti database atau framework, memastikan logika bisnis tidak bergantung pada detail implementasi teknis.

Artikel ini membahas konsep dasar Clean Architecture, lapisan-lapisan utamanya, serta alasan di balik desain ini yang membuat aplikasi lebih mudah diuji dan lebih tahan terhadap perubahan teknologi di masa depan.

Lapisan-Lapisan dalam Clean Architecture

Empat lapisan utama Clean Architecture dari dalam ke luar

// Dari dalam ke luar: // 1. Domain -> Entity, business rules murni, TANPA dependensi apa pun // 2. Application -> Use case, interface untuk repository // 3. Infrastructure -> Implementasi EF Core, API eksternal // 4. Presentation -> Controller, API endpoint
Domain berada di lapisan paling dalam dan paling abstrak, berisi entity dan aturan bisnis murni tanpa dependensi ke teknologi apa pun, sementara Infrastructure dan Presentation berada di lapisan luar yang berisi detail teknis konkret seperti EF Core dan ASP.NET Core yang bisa diganti tanpa memengaruhi logika bisnis inti di Domain.

Dependency Rule sebagai Aturan Utama

Dependency selalu mengarah ke dalam, tidak pernah ke luar

// BENAR: Infrastructure bergantung pada Domain namespace TokoOnline.Infrastructure; using TokoOnline.Domain; // Infrastructure TAHU tentang Domain // SALAH: Domain bergantung pada Infrastructure namespace TokoOnline.Domain; using TokoOnline.Infrastructure; // Domain TIDAK BOLEH tahu Infrastructure
Dependency Rule memastikan layer Domain yang berisi logika bisnis paling penting tidak pernah bergantung pada detail teknis seperti Entity Framework Core, sehingga logika bisnis tersebut tetap bisa diuji dan dipahami tanpa perlu mengetahui teknologi database apa yang dipakai di baliknya.

Interface sebagai Jembatan Antar Layer

Interface di Domain, implementasi di Infrastructure

// Domain mendefinisikan interface (abstraksi) namespace TokoOnline.Domain; public interface IProdukRepository { Task<Produk> GetByIdAsync(int id); } // Infrastructure mengimplementasikan interface tersebut namespace TokoOnline.Infrastructure; public class ProdukRepository : IProdukRepository { /* pakai EF Core di sini */ }
Interface IProdukRepository didefinisikan di layer Domain (atau Application), sementara implementasi konkretnya yang memakai EF Core berada di layer Infrastructure, memungkinkan Domain tetap tidak mengetahui apa pun tentang EF Core meski pada akhirnya operasi data memang memakai teknologi tersebut.

Manfaat Testability dari Pemisahan Layer

Logika bisnis murni mudah diuji tanpa setup kompleks

// Domain bisa diuji TANPA database, TANPA ASP.NET Core, // TANPA mock yang rumit, karena tidak bergantung apa pun [Fact] public void HitungDiskon_PelangganVip_Mendapat20Persen() { var pesanan = new Pesanan(isPelangganVip: true, total: 1000000); Assert.Equal(800000, pesanan.HitungTotalSetelahDiskon()); }
Karena Domain tidak bergantung pada teknologi apa pun, logika bisnis seperti perhitungan diskon bisa diuji dengan unit test sederhana tanpa perlu setup database, mock EF Core, atau konfigurasi ASP.NET Core yang kompleks, membuat test berjalan sangat cepat dan mudah dipelihara.

Alur Request Melewati Lapisan Clean Architecture

Controller sebagai pintu masuk yang memanggil use case, bukan mengakses database langsung

// Presentation (Controller) -> Application (Use Case) -> Domain -> Infrastructure public class ProdukController : ControllerBase { private readonly IBuatProdukUseCase _useCase; [HttpPost] public async Task<IActionResult> Buat(BuatProdukRequest req) { var hasil = await _useCase.Eksekusi(req.Nama, req.Harga); return Created($"/api/produk/{hasil.Id}", hasil); } } // Controller hanya meneruskan request, tidak tahu detail penyimpanan data

Clean Architecture mengatur bagaimana sebuah request mengalir melalui beberapa lapisan dengan tanggung jawab yang terpisah jelas. Controller pada lapisan Presentation hanya bertugas menerima input dari luar dan meneruskannya ke use case di lapisan Application, tanpa mengetahui bagaimana data sebenarnya disimpan atau diambil dari database.

Use case kemudian menjalankan aturan bisnis dengan memanggil entity dan logika domain yang berada di lapisan paling dalam, lalu berkomunikasi dengan Infrastructure melalui interface seperti IProdukRepository untuk operasi penyimpanan data. Dengan susunan ini, logika bisnis inti sama sekali tidak bergantung pada teknologi database atau framework web yang dipakai.

Manfaat nyata dari alur ini terlihat ketika aplikasi perlu diuji secara otomatis: use case bisa diuji dengan mengganti implementasi repository menggunakan mock atau in-memory storage, tanpa perlu menyalakan database sungguhan. Pengujian menjadi jauh lebih cepat dan tidak rapuh terhadap perubahan infrastruktur seperti migrasi dari SQL Server ke database lain.

Menguji Use Case Secara Terisolasi dengan Mock Repository

Mock repository memungkinkan use case diuji tanpa database nyata

// Unit test use case tanpa menyentuh database sungguhan public class BuatProdukUseCaseTest { [Fact] public async Task Eksekusi_HargaNegatif_HarusLemparException() { var mockRepo = new Mock<IProdukRepository>(); var useCase = new BuatProdukUseCase(mockRepo.Object); await Assert.ThrowsAsync<ArgumentException>( () => useCase.Eksekusi("Kabel HDMI", -1000)); mockRepo.Verify(r => r.Simpan(It.IsAny<Produk>()), Times.Never); } } // Test berjalan dalam hitungan milidetik, tanpa koneksi database

Salah satu manfaat paling nyata dari Clean Architecture adalah kemudahan menulis unit test untuk logika bisnis inti tanpa bergantung pada infrastruktur eksternal seperti database atau API pihak ketiga. Karena use case hanya berkomunikasi dengan repository lewat interface, test bisa membuat objek tiruan atau mock dari interface tersebut dan mengatur perilakunya sesuai skenario yang ingin diuji, tanpa perlu menyiapkan data sungguhan di SQL Server. Prinsip yang sama berlaku untuk pengujian handler query maupun validator, selama dependency-nya diakses lewat interface yang bisa digantikan dengan mock.

Pada contoh di atas, mock repository dipakai untuk memastikan bahwa ketika harga yang dikirim bernilai negatif, use case melempar exception sebelum sempat memanggil method Simpan sama sekali, yang diverifikasi lewat mockRepo.Verify dengan Times.Never. Pendekatan ini menguji perilaku dan interaksi, bukan hanya hasil akhir, sehingga bug seperti data tetap tersimpan meski validasi gagal bisa terdeteksi lebih awal. Skenario lain seperti data berhasil tersimpan saat input valid juga perlu diuji agar cakupan pengujian use case benar-benar menyeluruh.

Test semacam ini berjalan sangat cepat karena tidak ada I/O ke database atau jaringan, sehingga ratusan unit test bisa dijalankan dalam hitungan detik sebagai bagian dari continuous integration setiap kali kode di-push. Kecepatan ini mendorong developer lebih sering menjalankan test secara lokal sebelum commit, berbeda dengan test yang bergantung pada database sungguhan yang biasanya lebih lambat dan rapuh terhadap perubahan data environment. Praktik ini juga mendorong gaya test-driven development, karena test bisa ditulis lebih dulu sebelum implementasi use case benar-benar selesai.

Satu Use Case Mengorkestrasi Beberapa Langkah Bisnis Sekaligus

Use case tunggal mengatur urutan beberapa operasi terkait

// Use case mengoordinasikan beberapa langkah tanpa menaruh logikanya di controller public class BuatPesananUseCase { public async Task<int> Eksekusi(int pelangganId, List<int> produkId) { var pelanggan = await _pelangganRepo.CariById(pelangganId); var pesanan = Pesanan.Buat(pelanggan, produkId); await _pesananRepo.Simpan(pesanan); await _emailService.KirimKonfirmasi(pelanggan.Email, pesanan.Id); return pesanan.Id; } } // Controller cukup memanggil satu method ini, tanpa tahu urutan langkah di dalamnya

Satu use case sering kali perlu memanggil lebih dari satu repository atau service sekaligus dalam urutan tertentu, seperti mengambil data pelanggan, membuat pesanan, lalu mengirim email konfirmasi. Tanggung jawab mengoordinasikan urutan langkah-langkah ini ditempatkan di lapisan Application, bukan tersebar di Controller, agar alur bisnis tetap terbaca sebagai satu kesatuan yang jelas.

Dengan orkestrasi dipusatkan di use case, Controller cukup memanggil satu method saja tanpa perlu tahu detail langkah di dalamnya, dan logika yang sama bisa dipanggil ulang dari job terjadwal atau dari API lain tanpa duplikasi kode. Jika salah satu langkah gagal di tengah proses, use case juga menjadi tempat yang tepat untuk menentukan apakah langkah sebelumnya perlu dibatalkan.

Kesalahan Umum yang Sering Terjadi

Membiarkan Domain Bergantung pada Entity Framework Core

Menambahkan atribut atau base class dari Entity Framework Core langsung ke entity di layer Domain melanggar Dependency Rule, membuat logika bisnis inti bergantung pada detail teknis yang seharusnya bisa diganti tanpa memengaruhi Domain.

Menerapkan Clean Architecture untuk Project yang Terlalu Sederhana

Memaksakan struktur empat layer dengan banyak interface dan abstraksi untuk aplikasi CRUD sederhana yang kemungkinan besar tidak akan berganti teknologi bisa menambah kompleksitas yang tidak sepadan dengan manfaatnya.

Menaruh Logika Bisnis di Layer Infrastructure atau Presentation

Menulis logika bisnis penting langsung di controller atau repository implementation, alih-alih di Domain atau Application, menghilangkan manfaat utama Clean Architecture yaitu memisahkan logika bisnis dari detail teknis.

Kapan Pendekatan Ini Tidak Cocok Dipakai

Clean Architecture memberikan testability yang sangat baik dan ketahanan terhadap perubahan teknologi, sangat berharga untuk aplikasi enterprise berskala besar dengan umur panjang dan kemungkinan perubahan teknologi di masa depan.

Namun untuk project kecil atau prototipe cepat, overhead struktur dan abstraksi tambahan dari Clean Architecture bisa memperlambat development awal secara signifikan tanpa manfaat yang sepadan, sehingga pola ini paling cocok dipertimbangkan untuk project dengan skala dan umur yang memang membutuhkannya.

Ringkasan

Clean Architecture memisahkan logika bisnis inti dari detail teknis lewat Dependency Rule yang ketat, memastikan Domain tetap independen dan mudah diuji. Pertimbangkan kompleksitas tambahan ini secara matang, karena manfaatnya paling terasa pada aplikasi berskala besar dengan kebutuhan maintainability jangka panjang.

Seri Belajar REST API & Clean Architecture:

← Sebelumnya: Pagination Filtering dan Sorting pada REST API

→ Selanjutnya: Memisahkan Layer Domain Application dan Infrastructure (segera terbit)