Aplikasi monolit berbasis Laravel dan Inertia.js kerap mengalami degradasi struktur ketika basis kode membesar. Pendekatan flat MVC standar (menempatkan seluruh controller di app/Http/Controllers dan seluruh halaman frontend di resources/js/Pages) mencampuradukkan domain bisnis yang berbeda. Mengubah arsitektur menjadi Modular Monolith menyelesaikan masalah kepemilikan domain (bounded context), tetapi memunculkan trade-off baru pada resolusi dynamic import, overhead konfigurasi bundler (Vite), dan durasi eksekusi CI pipeline.
Flat MVC vs Modular Monolith Berbasis Bounded Context
Pada struktur flat MVC, batas modularitas hanya bersifat konseptual. Pengembang rentan memanggil model, service, atau komponen frontend lintas fitur tanpa batasan ketat. Akibatnya, dependensi sirkular backend dan tight coupling pada komponen frontend sulit dihindari.
Modular Monolith memecah aplikasi berdasarkan batas konteks domain (misal: Ordering, Billing, Inventory). Setiap modul mengisolasi logika backend sekaligus UI komponen miliknya. Namun, Inertia.js dirancang secara konvensional untuk membaca halaman dari satu root folder frontend tunggal, sehingga pemisahan modul menuntut penyesuaian pada page resolver frontend dan routing backend.
Struktur Direktori Modular Monolith
Berikut adalah tata letak direktori yang memisahkan modul backend dan frontend secara simetris:
app/
├── Modules/
│ ├── Billing/
│ │ ├── Controllers/
│ │ │ └── InvoiceController.php
│ │ ├── Models/
│ │ └── Routes/
│ │ └── web.php
│ └── Inventory/
│ ├── Controllers/
│ └── Routes/
resources/
├── js/
├── Modules/
│ ├── Billing/
│ │ ├── Pages/
│ │ │ └── InvoiceList.vue
│ │ └── Components/
│ └── Inventory/
│ ├── Pages/
│ └── Components/
├── Shared/ # Komponen UI generik (Button, Input, Modal)
└── app.jsKonfigurasi Resolusi Halaman dan Vite Alias
Inertia memerlukan penyesuaian pada fungsi resolver di app.js agar dapat memetakan format nama halaman bertingkat seperti Billing::InvoiceList atau Billing/InvoiceList.
Implementasi resolver modular pada frontend:
import { createApp, h } from 'vue';
import { createInertiaApp } from '@inertiajs/vue3';
import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers';
createInertiaApp({
resolve: (name) => {
// Pola nama: "Billing/Pages/InvoiceList" atau "Billing::InvoiceList"
const parts = name.split('::');
const module = parts[0];
const page = parts[1];
return resolvePageComponent(
`./Modules/${module}/Pages/${page}.vue`,
import.meta.glob('./Modules/**/Pages/**/*.vue')
);
},
setup({ el, App, props, plugin }) {
createApp({ render: () => h(App, props) })
.use(plugin)
.mount(el);
},
});Tambahkan alias path pada vite.config.js untuk menyederhanakan import antar-modul dan mencegah relative path yang rapuh (../../../../):
import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
import vue from '@vitejs/plugin-vue';
import path from 'path';
export default defineConfig({
plugins: [
laravel({
input: 'resources/js/app.js',
refresh: true,
}),
vue(),
],
resolve: {
alias: {
'@shared': path.resolve(__dirname, 'resources/js/Shared'),
'@modules': path.resolve(__dirname, 'resources/js/Modules'),
},
},
});Isolasi Shared Props: Menghindari Global Pollution
Pada instalasi standar Inertia, HandleInertiaRequests middleware menyediakan shared props global (seperti autentikasi, flash messages, metadata aplikasi). Dalam skala modular, middleware ini sering kali menjadi dumping ground bagi data yang hanya dibutuhkan oleh modul tertentu.
Hindari menambahkan data domain-spesifik ke dalam root middleware. Pisahkan data spesifik modul melalui middleware per-rute atau Inertia Share lokal:
namespace App\Modules\Billing\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Inertia\Inertia;
class ShareBillingContext
{
public function handle(Request $request, Closure $next)
{
Inertia::share([
'billing_quota' => fn () => $request->user()?->billingQuota(),
]);
return $next($request);
}
}Daftarkan middleware tersebut secara eksklusif pada grup route Modules/Billing/Routes/web.php. Cara ini memastikan payload JSON Inertia tetap ramping saat pengguna berada di luar konteks domain Billing.
Strategi Vite Chunking: Dynamic Import vs Monolithic Bundle
Penggunaan import.meta.glob di resolver Inertia secara default memicu pemisahan kode otomatis (code splitting) untuk setiap halaman. Namun, arsitektur modular monolith menghasilkan tantangan unik pada distribusi vendor dan shared dependencies.
Overhead Dynamic Import vs Monolithic Bundle
- Dynamic Chunking (Default): Memecah setiap page modul menjadi file
.jsterpisah. Keuntungan: initial load time aplikasi kecil. Kerugian: HTTP request overhead bertambah ketika halaman memerlukan banyak chunk bersama (shared modules/vendor), memicu network waterfall jika tidak dikonfigurasi dengan HTTP/2 multiplexing atau preloading. - Domain-Level Chunking: Mengelompokkan seluruh halaman dalam satu domain ke dalam satu bundle modul menggunakan
manualChunkspada Rollup.
Optimasi chunking domain pada vite.config.js:
build: {
rollupOptions: {
output: {
manualChunks(id) {
if (id.includes('resources/js/Modules/Billing/')) {
return 'module-billing';
}
if (id.includes('resources/js/Modules/Inventory/')) {
return 'module-inventory';
}
if (id.includes('node_modules')) {
return 'vendor';
}
},
},
},
}Dampak Build Time, CI Pipeline, dan Footprint Server
Transisi ke modular monolith berdampak langsung pada operasional sistem:
- Vite Cold Build Duration: Semakin banyak file
Pages/**/*.vueyang dipindai oleh glob pattern, komputasi grafik dependensi Rollup akan meningkat secara linier. Proyek dengan 300+ halaman modular mengalami kenaikan durasi build dari kisaran detik ke hitungan menit. - Durasi CI Pipeline: Analisis statis (PHPStan/Psalm pada PHP, ESLint/TypeScript pada frontend) membutuhkan parsing path ganda. Jika batas antar-modul diverifikasi menggunakan tool seperti Deptrac (PHP) dan rules ESLint import/no-restricted-paths, durasi pipeline CI meningkat sekitar 25% hingga 40%.
- Server Footprint: Asset bundling modular menghasilkan file chunk yang lebih banyak di disk server (
public/build/assets/). Cache invalidation menjadi lebih efisien karena perubahan kode di modulInventorytidak membatalkan cache chunk modulBilling.
Evaluasi Batas Kelayakan: Kapan Modularitas Dibutuhkan?
Pemisahan modul bukan keputusan tanpa biaya. Pola ini memicu cognitive friction jika diterapkan sebelum waktunya.
Tanda Over-engineering:
- Aplikasi dikelola oleh tim tunggal berisi kurang dari 5 engineer.
- Skema database antar-modul masih sangat terikat via Foreign Key langsung tanpa abstraksi layer service/repository.
- Lebih banyak waktu dihabiskan untuk mengatur path alias, dependency boundaries, dan shared props daripada menulis logika bisnis.
Kondisi Wajib Mengadopsi Modular Monolith:
- Dikerjakan oleh multi-tim otonom yang sering mengalami merge conflict pada routing, root controller, dan frontend pages.
- Kebutuhan audit kepemilikan kode (code ownership) berbasis domain bisnis.
- Persiapan migrasi jangka panjang menuju microservices, di mana domain boundaries backend dan frontend perlu divalidasi terlebih dahulu di dalam lingkungan monolitik yang aman.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!