34 modul dalam satu monolith: pelajaran dari nwidart/laravel-modules
ERP 34 modul dan School HRIS 9 package group jalan dalam satu Laravel app. Begini cara nwidart-style modules bikin itu tidak jadi spaghetti.
Context
Dua project terakhir yang aku pegang sama-sama modular: ERP dengan 34 modul (Accounting, POS, Commerce, Payroll, CRM, dan lainnya), dan School HRIS dengan 9 package group (Kepegawaian, Presensi, Akademik, Al-Quran, dan lainnya).
Keduanya jalan dalam satu Laravel app. Satu codebase, satu deploy. Bukan microservices, bukan multi-repo.
Pertanyaan yang selalu muncul: gimana caranya 34 modul tidak saling injek jadi spaghetti?
Options yang dipertimbangkan
Option A: Struktur default Laravel
Semua model di app/Models, semua controller di app/Http/Controllers. Folder dibagi per layer, bukan per domain.
- Pro: zero setup, semua dev Laravel familiar
- Con: di 34 modul,
app/Modelsisinya ratusan file. Nyari file = ctrl+P dan berdoa - Con: tidak ada boundary. Controller modul Sale bebas query model modul Payroll langsung
Option B: Laravel packages via Composer path repository
Tiap modul jadi package Composer sendiri di folder packages/.
- Pro: isolation paling kuat, bisa dipublish terpisah kalau perlu
- Con: overhead Composer per modul. Update satu file = dump-autoload
- Con: tidak ada integrasi bawaan dengan Filament. Panel provider harus di-wire manual per package
Option C: nwidart-style modules (yang dipilih)
Pakai nwidart/laravel-modules dan variannya (coolsam/modules untuk integrasi Filament).
- Pro: tiap modul self-contained — migrations, models, Filament resources, service provider, routes, semua dalam satu folder
- Pro: coolsam/modules kasih first-class Filament integration. Resource auto-discover per modul
- Con: konvensi tambahan yang harus dipahami dev baru
Struktur satu modul
Modules/
Sale/
Database/Migrations/
Models/
Filament/Resources/
Http/Controllers/
Providers/SaleServiceProvider.php
Routes/
Modul yang tidak dipakai client tinggal di-disable di config. Tidak perlu hapus kode.
Komunikasi antar modul: events, bukan direct call
Aturan yang aku pegang keras: modul tidak boleh call model modul lain langsung. Lewat events.
Contoh nyata dari School HRIS: modul Kepegawaian fire EmployeeHired event. Modul Presensi listen dan auto-buat attendance profile untuk karyawan baru itu.
// Modules/Kepegawaian — fire event, tidak tahu siapa yang dengar
event(new EmployeeHired($employee));
// Modules/Presensi — listener
class CreateAttendanceProfile
{
public function handle(EmployeeHired $event): void
{
AttendanceProfile::firstOrCreate([
'employee_id' => $event->employee->id,
]);
}
}
Kepegawaian tidak tahu Presensi ada. Presensi tidak tahu detail Kepegawaian. Kalau modul Presensi di-disable, Kepegawaian tetap jalan tanpa error.
Ini juga menghindari N+1 tersembunyi: listener bisa queue job, query dilakukan sekali per event, bukan berulang di controller.
Consequences
Impact nyata yang aku rasakan:
- Onboarding dev baru: butuh waktu ekstra untuk paham konvensi modul. Setelah paham, navigasi codebase jadi lebih cepat daripada struktur default — semua yang berhubungan ada di satu folder.
- Testing: tiap modul bisa ditest isolated. Test modul Accounting tidak perlu boot modul Commerce.
- Client flexibility: client ERP bisa pilih subset modul. Modul yang tidak dibeli tidak aktif, tapi kodenya tetap satu repo.
- Refactor cost: rename concept lintas modul tetap sakit. Event contract jadi public API yang harus dijaga.
Trade-off yang aku terima
Modular monolith bukan silver bullet:
- Boundary dijaga oleh disiplin, bukan oleh network. Tidak ada yang nge-block developer nakal query lintas modul. Solusiku: code review + dokumentasi konvensi, bukan tooling.
- Satu modul error fatal tetap bisa jatuhkan satu app. Mitigasi: Sentry per modul tagging, Telescope aktif di staging.
- Shared kernel (user, auth, master data) tetap ada dan jadi titik coupling tertinggi.
What I’d do differently
- Definisikan event contract sejak awal sebagai class final dengan property readonly. Refactor event payload di tengah jalan menyakitkan karena listener tersebar di banyak modul.
- Setup modul health check lebih awal — cara cepat tahu modul mana yang aktif di environment mana.
References
- nwidart/laravel-modules — package utama, dokumentasi lengkap
- Case study: 34-Module ERP dan School HRIS — dua implementasi nyata
- Microservices bukan jawaban untuk tim kecil — kenapa 34 modul ini tetap satu app
- Participant polymorphic — pattern relasi data di dalam modul School HRIS