Clean Architecture dan Domain-Driven Design pada Aplikasi Laravel Enterprise
Clean Architecture dan Domain-Driven Design pada Aplikasi Laravel Enterprise
1. Konteks Industri & Analisis Akar Masalah
Dalam lanskap pengembangan perangkat lunak modern, framework seperti Laravel telah menjadi standar industri untuk membangun aplikasi web dengan cepat. Laravel menawarkan ekosistem yang luar biasa kaya dengan fitur out-of-the-box seperti Eloquent ORM, Service Container, Artisan CLI, dan berbagai paket resmi seperti Sanctum, Horizon, dan Telescope. Namun, kemudahan ini sering kali menjadi pedang bermata dua pada aplikasi skala enterprise.
Aplikasi enterprise dicirikan oleh kompleksitas domain bisnis yang tinggi, umur aplikasi yang panjang (years-to-decades), dan tim pengembang yang terus berganti. Ketika sebuah aplikasi Laravel dikembangkan secara naifβmengikuti pola default framework tanpa batasan arsitektural yang ketatβdeveloper cenderung menempatkan seluruh logika bisnis (business logic) di dalam Controller, Model (Eloquent), atau Service Provider. Fenomena ini melahirkan apa yang dikenal sebagai Fat Controllers dan God Models.
Dampak negatif dari pendekatan ini sangat destruktif terhadap skalabilitas jangka panjang:
- Tough Coupling dengan Framework: Logika domain terikat secara permanen dengan infrastruktur Laravel dan Eloquent ORM. Ketika versi Laravel melakukan breaking changes, migrasi menjadi mimpi buruk.
- Ketidakmampuan Melakukan Unit Testing: Menguji logika bisnis murni menjadi mustahil tanpa mendirikan koneksi database, menjalankan HTTP kernel, atau memuat seluruh service container Laravel, yang membuat tes menjadi sangat lambat (slow integration tests).
- Regresi Bisnis: Peraturan bisnis yang tersebar di berbagai lifecycle hooks Eloquent (
saving,updating) atau event listeners membuat alur eksekusi tidak dapat diprediksi.
Perbandingan Kondisi: Sebelum vs Sesudah Penerapan Clean Architecture & DDD
| Parameter | Sebelum (Default Laravel Monolith) | Sesudah (Clean Architecture + DDD) |
|---|---|---|
| Lokasi Logika Bisnis | Tersebar di Controller, Eloquent Model, dan Observer. | Terpusat di Domain Layer (Entities, Value Objects, Domain Services). |
| Ketergantungan Database | Sangat kuat pada Eloquent ORM & Active Record Pattern. | Diisolasi melalui Repository Interface dan Data Mapper (Persistence Layer). |
| Waktu Eksekusi Unit Test | Lambat (>5 detik karena memuat database/framework). | Sangat cepat (<0.01 detik per tes, murni PHP tanpa framework overhead). |
| Maintainability | Menurun drastis seiring bertambahnya ukuran kode (Spaghetti Code). | Stabil dan modular; perubahan infrastruktur tidak merusak domain. |
2. Konsep Inti & Deep Dive Arsitektur
Clean Architecture (dipopulerkan oleh Robert C. Martin / Uncle Bob) dan Domain-Driven Design (dipelopori oleh Eric Evans) memiliki tujuan yang selaras: menempatkan domain bisnis di jantung aplikasi dan melindungi domain tersebut dari dunia luar (framework, database, UI, third-party APIs).
Aturan Ketergantungan (The Dependency Rule)
Ketergantungan kode hanya boleh mengarah ke dalam (inward). Lingkaran luar boleh mengetahui lingkaran dalam, tetapi lingkaran dalam tidak boleh tahu apa-apa tentang lingkaran luar. Domain (Enterprise Business Rules) berada di pusat, dikelilingi oleh Application Business Rules (Use Cases), yang kemudian dikelilingi oleh Interface Adapters (Controllers, Presenters, Gateways), dan paling luar adalah Frameworks & Drivers (Web Server, Database, UI).
+-------------------------------------------------------------+
| INFRASTRUCTURE |
| [Laravel Controllers] [Eloquent Models] [MySQL / Redis] |
| +-----------------------------------------+ |
| | APPLICATION | |
| | [Use Cases / Interactors] [DTOs] | |
| | +---------------------+ | |
| | | DOMAIN | | |
| | | [Entities] | | |
| | | [Value Objects] | | |
| | | [Domain Services] | | |
| | | [Repository Intf] | | |
| | +---------------------+ | |
| +-----------------------------------------+ |
+-------------------------------------------------------------+
Lapisan Utama dalam Aplikasi
- Domain Layer: Inti dari sistem. Berisi Entities (objek dengan identitas unik dan siklus hidup), Value Objects (objek immutable yang didefinisikan oleh atributnya), Domain Events, dan Repository Interfaces.
- Application Layer: Mengorkorstrasi alur data untuk tugas-tugas aplikasi. Berisi Use Cases atau Command/Query Handlers. Lapisan ini tidak berisi aturan bisnis murni, melainkan aturan alur kerja aplikasi (misal: ambil data dari repository, panggil domain entity, simpan kembali).
- Infrastructure Layer: Berisi implementasi konkret dari interface yang didefinisikan di dalam. Di sinilah Laravel hidup: Eloquent Models, Database Migrations, Third-party API Clients (Guzzle), Mailers, dan Controllers.
3. Perbandingan Komparatif / Analisis Pendekatan
Memilih untuk menerapkan Clean Architecture pada Laravel menuntut trade-off yang harus disadari oleh arsitek sistem:
- Overhead Boilerplate: Anda akan menulis lebih banyak kelas (DTO, Interfaces, Mappers, Value Objects). Sebuah operasi sederhana yang biasanya butuh 1 Controller kini membutuhkan Request DTO, Command, Command Handler, Repository, dan Entity Mapper.
- Kurva Belajar (Learning Curve): Tim yang terbiasa dengan kecepatan RAD (Rapid Application Development) Laravel akan mengalami gesatan produktivitas di awal.
- Keuntungan Performa & Skalabilitas: Meskipun ada sedikit overhead memori untuk instansiasi objek tambahan, isolasi logika bisnis membuat optimasi tingkat lanjut (seperti memindahkan domain logic ke worker asynchronous atau microservice terpisah) menjadi jauh lebih mudah.
4. Prasyarat Sistem & Persiapan Lingkungan
Untuk memastikan implementasi berjalan mulus pada lingkungan production enterprise, pastikan infrastruktur memenuhi spesifikasi berikut:
- PHP: Versi 8.3+ dengan JIT compiler diaktifkan.
- Database: MySQL 8.0+ atau PostgreSQL 15+.
- Cache & Queue: Redis 7.x.
- Web Server: Nginx dengan PHP-FPM (Opcache diaktifkan).
Struktur Direktori Kustom (Di luar direktori default Laravel)
Kita akan memisahkan domain di luar direktori app/ atau membuat namespace khusus di dalam app/Core/ agar tetap kompatibel dengan autoloader Composer.
app/
βββ Core/
β βββ Domain/
β β βββ Order/
β β β βββ Entities/
β β β βββ ValueObjects/
β β β βββ Exceptions/
β β β βββ Repositories/
β βββ Application/
β βββ Order/
β βββ Commands/
β βββ DTOs/
βββ Infrastructure/
βββ Persistence/
β βββ Eloquent/
β βββ Repositories/
βββ Http/
βββ Controllers/
5. Panduan Implementasi Langkah demi Langkah
[!IMPORTANT] Domain Layer tidak boleh mengimpor kelas apa pun dari namespace
Illuminate*atau namespace infrastruktur lainnya.
1. Implementasi Value Object pada Domain Layer
Buat Value Object untuk merepresentasikan harga moneter (Money) guna menghindari masalah presisi floating point.
<?php
declare(strict_types=1);
namespace App\Core\Domain\Order\ValueObjects;
use InvalidArgumentException;
final class Money
{
private int $amount;
private string $currency;
public function __construct(int $amount, string $currency = 'IDR')
{
if ($amount < 0) {
throw new InvalidArgumentException("Amount cannot be negative.");
}
$this->amount = $amount;
$this->currency = $currency;
}
public function getAmount(): int
{
return $this->amount;
}
public function getCurrency(): string
{
return $this->currency;
}
public function add(Money $other): self
{
if ($this->currency !== $other->currency) {
throw new InvalidArgumentException("Currency mismatch.");
}
return new self($this->amount + $other->amount, $this->currency);
}
}
2. Implementasi Domain Entity
<?php
declare(strict_types=1);
namespace App\Core\Domain\Order\Entities;
use App\Core\Domain\Order\ValueObjects\Money;
use App\Core\Domain\Order\Exceptions\OrderAlreadyPaidException;
final class Order
{
private string $id;
private Money $totalAmount;
private string $status;
public function __construct(string $id, Money $totalAmount, string $status = 'pending')
{
$this->id = $id;
$this->totalAmount = $totalAmount;
$this->status = $status;
}
public function pay(): void
{
if ($this->status === 'paid') {
throw new OrderAlreadyPaidException("Order is already paid.");
}
$this->status = 'paid';
}
public function getId(): string
{
return $this->id;
}
public function getTotalAmount(): Money
{
return $this->totalAmount;
}
public function getStatus(): string
{
return $this->status;
}
}
3. Repository Interface di Domain Layer
<?php
declare(strict_types=1);
namespace App\Core\Domain\Order\Repositories;
use App\Core\Domain\Order\Entities\Order;
interface OrderRepositoryInterface
{
public function findById(string $id): ?Order;
public function save(Order $order): void;
}
4. Implementasi Konkret pada Infrastructure Layer (Eloquent Repository)
<?php
declare(strict_types=1);
namespace App\Infrastructure\Persistence\Repositories;
use App\Core\Domain\Order\Entities\Order;
use App\Core\Domain\Order\Repositories\OrderRepositoryInterface;
use App\Core\Domain\Order\ValueObjects\Money;
use App\Models\OrderModel as EloquentOrder;
final class EloquentOrderRepository implements OrderRepositoryInterface
{
public function findById(string $id): ?Order
{
$eloquentOrder = EloquentOrder::find($id);
if (!$eloquentOrder) {
return null;
}
return new Order(
$eloquentOrder->id,
new Money((int) $eloquentOrder->total_amount, $eloquentOrder->currency),
$eloquentOrder->status
);
}
public function save(Order $order): void
{
EloquentOrder::updateOrCreate(
['id' => $order->getId()],
[
'total_amount' => $order->getTotalAmount()->getAmount(),
'currency' => $order->getTotalAmount()->getCurrency(),
'status' => $order->getStatus(),
]
);
}
}
5. Binding Interface ke Laravel Service Container
Daftarkan binding repository di dalam AppServiceProvider atau AppProvidersAppServiceProvider.
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use App\Core\Domain\Order\Repositories\OrderRepositoryInterface;
use App\Infrastructure\Persistence\Repositories\EloquentOrderRepository;
class RepositoryServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->bind(
OrderRepositoryInterface::class,
EloquentOrderRepository::class
);
}
}
6. Bedah Kode & Penjelasan Detail Baris Kunci
declare(strict_types=1);: Wajib disematkan di setiap file PHP untuk memaksa PHP melakukan pemeriksaan tipe data secara ketat (strict typing), mencegah bug implisit dari konversi tipe otomatis PHP.Immutable Value Object: Objek sepertiMoneytidak memiliki metode setter. Jika ingin mengubah nilai, objek baru harus dibuat (new self(...)). Ini menjamin thread-safety dan mencegah mutasi state yang tidak disengaja di tengah siklus eksekusi request.Data Mapping: Perhatikan bagaimanaEloquentOrderRepositorybertindak sebagai penerjemah (mapper) antara model database (EloquentOrder) dan Domain Entity (Order). Hal ini memastikan domain murni tidak tahu menahu tentang Eloquent.
7. Skenario Produksi & Studi Kasus Dunia Nyata
Pada sistem e-commerce skala besar dengan ribuan transaksi per detik (high concurrency), pemisahan domain ini memungkinkan kita menerapkan pola CQRS (Command Query Responsibility Segregation) dengan mudah. Operasi tulis (Write Operations) dieksekusi melalui Domain Model yang ketat dengan transaksi database berisolasi tinggi, sementara operasi baca (Read Operations) dapat diarahkan langsung ke read-replica menggunakan query SQL mentah atau view terdenormalisasi tanpa melewati beban instansiasi Domain Entity.
8. Common Pitfalls, Anti-Patterns & Solusi
- Anti-Pattern: Eloquent Model di Domain Layer
- Masalah: Memasukkan instance Eloquent Model ke dalam Entity Domain.
- Solusi: Selalu petakan data Eloquent ke murni PHP Domain Entity menggunakan mapper.
- Pitfall: Menggunakan Database Transaction di dalam Domain Entity
- Masalah: Domain entity memanggil
DB::beginTransaction(), yang mengikat domain ke framework database driver. - Solusi: Letakkan manajemen transaksi di Application Layer (Use Case / Command Handler) atau gunakan Database Transaction Middleware.
- Masalah: Domain entity memanggil
9. Benchmark, Metrik Kinerja & Tuning Performa
Pengujian unit test domain murni menggunakan PHPUnit dapat dioptimalkan dengan menjalankan test suite tanpa memuat container Laravel sama sekali (--dont-discover atau membuat test case khusus yang mengextend PHPUnitFrameworkTestCase alih-alih TestsTestCase).
Contoh benchmark eksekusi tes unit Domain:
vendor/bin/phpunit tests/Unit/Domain/OrderTest.php
Hasil: 50 test case domain murni selesai dieksekusi dalam waktu 0.04 detik.
Tuning tambahan pada PHP-FPM untuk production:
; /etc/php/8.3/fpm/pool.d/www.conf
pm = dynamic
pm.max_children = 100
pm.start_servers = 20
pm.min_spare_servers = 10
pm.max_spare_servers = 30
pm.max_requests = 500
10. Checklist Penerapan Produksi & Kesimpulan
- Seluruh aturan ketergantungan (Dependency Rule) divalidasi menggunakan static analyzer seperti PHPStan (Level Max).
- Tidak ada namespace
Illuminateyang bocor ke dalam folderApp/Core/Domain. - Repository interface terikat dengan benar di Service Container.
- Unit test untuk Value Objects dan Entities mencapai 100% test coverage.
- Konfigurasi OPcache dan PHP-FPM di-tuning sesuai kapasitas RAM server production.
Dengan menerapkan Clean Architecture dan DDD pada Laravel, aplikasi enterprise Anda tidak lagi menjadi sandera dari satu framework tertentu, melainkan sistem yang kokoh, modular, mudah diuji, dan siap menghadapi dekade pertumbuhan bisnis berikutnya.