Modularisasi kode
Aplikasi yang tumbuh selalu sampai pada pertanyaan yang sama: bagaimana supaya folder app/ yang berisi tiga ratus berkas tetap bisa dinavigasi. Jawabannya jarang berupa layanan terpisah.
Intisari
- Modular monolith: satu aplikasi, satu deploy, tapi batas modul yang jelas dan ditegakkan.
- Modul dipisahkan berdasarkan domain bisnis, bukan berdasarkan jenis kelas.
- Batasnya ditegakkan otomatis lewat uji arsitektur Pest โ bukan lewat kesepakatan lisan.
- Package lokal (
pathrepository di Composer) memberi batas yang paling tegas. - Microservice memindahkan kompleksitas dari kode ke jaringan. Bayar hanya kalau kamu memang mendapat sesuatu.
Dua cara memecah kode
BERDASARKAN JENIS (default Laravel) BERDASARKAN DOMAIN
app/ app/
โโโ Models/ โโโ Redaksi/
โ โโโ Artikel.php โ โโโ Models/Artikel.php
โ โโโ Iklan.php โ โโโ Http/ArtikelController.php
โ โโโ Pengguna.php โ โโโ Actions/TerbitkanArtikel.php
โโโ Http/Controllers/ โ โโโ Policies/ArtikelPolicy.php
โ โโโ ArtikelController.php โโโ Iklan/
โ โโโ IklanController.php โ โโโ โฆ
โ โโโ PenggunaController.php โโโ Akun/
โโโ Policies/ โโโ โฆ
Kapan pemecahan berdasarkan jenis mulai gagal. Selama aplikasimu punya sepuluh model, folder
Models/ lebih mudah dinavigasi. Setelah lima puluh model dari lima area bisnis yang berbeda, kamu
harus membuka empat folder untuk memahami satu fitur โ dan tidak ada yang bisa menjawab "kalau saya mengubah
ini, apa lagi yang terpengaruh". Pemecahan berdasarkan domain menjawab pertanyaan itu dengan struktur folder.
Isi sebuah modul
app/Redaksi/
โโโ Models/ Artikel, Kategori, Revisi
โโโ Http/
โ โโโ Controllers/
โ โโโ Requests/
โ โโโ Resources/
โโโ Actions/ TerbitkanArtikel, ArsipkanArtikel
โโโ Events/ ArtikelTerbit
โโโ Jobs/ BuatRingkasan
โโโ Policies/
โโโ RedaksiServiceProvider.php
โโโ routes.php
// app/Redaksi/RedaksiServiceProvider.php
class RedaksiServiceProvider extends ServiceProvider
{
public function boot(): void
{
Route::middleware('web')->group(__DIR__.'/routes.php');
$this->loadViewsFrom(__DIR__.'/resources/views', 'redaksi');
$this->loadMigrationsFrom(__DIR__.'/database/migrations');
Gate::policy(Artikel::class, ArtikelPolicy::class);
}
}
Menegakkan batas
// tests/Feature/ArsitekturTest.php
arch('modul Iklan tidak boleh menyentuh internal Redaksi')
->expect('App\Iklan')
->not->toUse('App\Redaksi\Actions');
arch('modul hanya berkomunikasi lewat event dan kontrak publik')
->expect('App\Redaksi\Http')
->not->toUse(['App\Akun\Models', 'App\Iklan\Models']);
arch('controller tidak menyentuh database langsung')
->expect('App\Redaksi\Http\Controllers')
->not->toUse('Illuminate\Support\Facades\DB');
Inilah yang membedakan modularisasi sungguhan dari sekadar memindahkan berkas. Tanpa penegakan, batas modul akan luntur dalam hitungan bulan โ seseorang akan memanggil kelas dari modul lain karena sedang buru-buru, dan tidak ada yang menyadarinya saat review. Uji arsitektur mengubah kesepakatan jadi gerbang yang tidak bisa dilewati diam-diam.
Komunikasi antar modul
| Cara | Kopling | Kapan |
|---|---|---|
| Event | Paling longgar | "Sesuatu terjadi" โ modul lain boleh bereaksi |
| Kontrak publik (interface) | Sedang | Modul A butuh jawaban dari modul B |
| Memanggil kelas internal | Terlalu erat | Jangan โ inilah yang dicegah uji arsitektur |
| Berbagi tabel database | Paling erat | Kadang tidak terhindarkan; sadari harganya |
// Modul Redaksi mengumumkan; ia tidak tahu siapa yang mendengarkan
ArtikelTerbit::dispatch($artikel);
// Modul Iklan mendengarkan; Redaksi tidak perlu tahu Iklan itu ada
class SegarkanSlotIklan
{
public function handle(ArtikelTerbit $event): void { /* ... */ }
}
Package lokal: batas yang paling tegas
// composer.json
"repositories": [
{ "type": "path", "url": "modules/*" }
],
"require": {
"portal/redaksi": "*"
}
Dengan package lokal, modul punya composer.json-nya sendiri โ dan dependensinya jadi eksplisit.
Modul yang tidak mendeklarasikan ketergantungan pada modul lain tidak bisa memakainya, karena
autoload-nya memang tidak menemukan kelasnya. Ini batas yang ditegakkan oleh alat, bukan oleh disiplin.
Harganya: setiap modul jadi punya siklus hidupnya sendiri. Menambah dependensi berarti mengedit dua
composer.json; menjalankan tes berarti memikirkan modul mana. Untuk tim besar dengan kepemilikan
yang jelas per modul, itu sepadan. Untuk tim lima orang, folder plus uji arsitektur biasanya sudah cukup dan
jauh lebih ringan.
Kenapa bukan microservice
| Yang hilang saat memecah jadi layanan terpisah |
|---|
| Transaksi database yang mencakup beberapa domain |
| Join sederhana antar tabel yang tadinya bertetangga |
| Refactor lintas modul dalam satu commit |
| Satu deploy, satu rollback |
| Debugging dengan satu stack trace |
| Panggilan yang tidak bisa gagal karena jaringan |
Microservice memindahkan kompleksitas, bukan menghapusnya. Ia memindahkannya dari struktur kode ke jaringan โ tempat kegagalan bersifat parsial, latensi bertambah, dan konsistensi jadi masalah yang harus kamu rancang sendiri. Alasan yang sah untuk membayar itu ada: tim yang benar-benar terpisah dan perlu merilis sendiri-sendiri, atau satu bagian yang kebutuhan penskalaannya sangat berbeda. "Supaya rapi" bukan salah satunya. Monolith modular memberi hampir seluruh manfaat organisasinya tanpa satu pun biaya jaringannya.
Jalur yang masuk akal
- Mulai dengan struktur Laravel biasa. Jangan membangun modul untuk aplikasi yang belum punya pengguna.
- Saat satu area sudah punya lebih dari sekitar sepuluh model dan pemiliknya jelas, pindahkan ke modul.
- Tegakkan batasnya dengan uji arsitektur pada hari yang sama.
- Komunikasi antar modul lewat event dan kontrak, tidak pernah lewat kelas internal.
- Baru pertimbangkan layanan terpisah kalau ada alasan operasional yang nyata โ bukan alasan estetika.
Latihan: pindahkan satu area aplikasimu ke app/<Domain>/ dengan service provider
dan berkas rutenya sendiri. Lalu tulis satu arch() yang melarang modul lain memakai folder
Actions-nya, dan buktikan tesnya gagal saat kamu sengaja melanggarnya.
Rangkuman ini sengaja dipangkas ke bagian yang dipakai di roadmap. Buka sumber aslinya saat kamu butuh detail lengkap atau referensi parameter.