Modular monolith: package by feature vs by layer, module boundary dan public API antar modul, data isolation per modul, dan jalan menuju microservices — feature modules NestJS dengan barrel export, Go package per bounded capability, laravel-modules — praktik pecah monolit menjadi 4 modul users orders products billing

Layered (episode 13) memberi disiplin teknologi tapi membiarkan domain bercampur: service orders bebas membaca tabel billing, controller users mengimpor repo products. Modular Monolith menambahkan dimensi yang hilang itu: sistem tetap satu deployment, namun di dalamnya hidup modul-modul dengan batas tegas.
Pola ini adalah jawaban industri 2026 atas "microservices hangover" — dan fondasi terbaik jika kelak memang perlu microservices (episode 26 membahas trennya).
BY LAYER (ep.13): BY FEATURE/MODULE (ep.14):
├── controllers/ ├── users/
│ ├── OrdersController.ts │ ├── api/ # public API modul
│ └── UsersController.ts │ ├── internal/ # detail tersembunyi
├── services/ │ └── domain/
│ ├── OrderService.ts ├── orders/
│ └── UserService.ts │ ├── api/
├── repositories/ │ ├── internal/
│ ... │ └── domain/
├── products/ ...
Kohesi per TEKNOLOGI Kohesi per DOMAIN/BISNISAturan modular monolith: package by feature di level atas, layered DI DALAM tiap modul. Modul orders berisi layer sendiri; yang dipertukarkan antar modul hanya lewat gerbang publiknya.
Tiga aturan boundary:
orders/api/index.ts, package ordersapi di Go).internal/ tidak diimpor lintas modul; pelanggaran = coupling tersembunyi.Aturan paling keras sekaligus paling bernilai: modul lain dilarang menyentuh tabel milik modul. Ingin data order? Minta lewat API modul orders atau langganan event-nya — jangan query langsung tabel orders. Isolasi inilah yang membuat ekstraksi ke microservices kelak tinggal "angkat satu folder + ganti panggilan lokal jadi RPC".
// src/orders/orders.module.ts
@Module({
imports: [DatabaseModule],
controllers: [OrdersController],
providers: [CheckoutService, PrismaOrdersRepository],
exports: [CheckoutService], // SATU-SATunya yang bocor keluar
})
export class OrdersModule {}
// src/orders/index.ts - PUBLIC API modul:
export { CheckoutService } from './services/checkout.service';
export { PlaceOrderCommand } from './api/place-order.command';
// TANPA export PrismaOrdersRepository/entity internal!
// src/users/services/register-user.service.ts
constructor(
private checkout: CheckoutService, // via import dari '@app/orders'
) {}Target outline: pecah monolit layered menjadi 4 modul — users, orders, products, billing — komunikasi via public API.
1. Buat skeleton 4 modul (folder + file public API kosong)
2. Pindahkan kode fitur ke modulnya masing-masing
(git mv agar riwayat ikut!)
3. Petakan dependency lintas-fitur lama:
grep -rn "from '.*users'" src/orders -> ganti jadi import public API
4. Tegakkan aturan tabel:
- billing TIDAK BOLEH query tabel orders
- kebutuhan data order? panggil orders.PublicAPI atau subscribe event
5. Lint gate baru (dependency-cruiser): dilarang import
*/internal/* atau */domain/* dari modul lain
6. Suite hijau + commit "ep14: split into modules"Ujian akhir: coba bayangkan modul orders diekstrak jadi microservice — karena semua komunikasinya sudah via public API/event, daftar perubahannya pendek. Itu bukti modular monolith kalian benar.
Warning
Modular tanpa penegakan otomatis hanyalah keinginan baik. Tanpa lint/compiler gate, dalam dua sprint pasti ada yang mengimpor internal — dan boundary runtuh diam-diam.
Rangkuman episode ini:
FASE 3 tuntas! Episode 15 membuka FASE 4 advanced: Hexagonal Architecture (Ports & Adapters) — domain di tengah, plug-in/plug-out infrastruktur, dan swap Postgres→in-memory untuk testing. Sampai jumpa!