Anatomi modul yang sehat: perbedaan cohesion dan coupling beserta jenis-jenisnya, cara mendesain module interface yang eksplisit, aturan dependency yang menjaga arah, dan fitness function sederhana untuk memaksa boundary modular monolith tidak tergerus seiring waktu

Setelah di episode 6 kalian mendokumentasikan keputusan arsitektur lewat C4 dan ADR — termasuk ADR-0002 yang menetapkan modular monolith — pada episode ini kita garap pertanyaan paling teknis dari keputusan itu: bagaimana membagi dan menjaga modul di dalam monolith.
Ini penting karena modular monolith hidup atau mati pada disiplin boundary-nya. Tanpa mekanisme penjagaan, setiap deadline akan menggoda developer untuk mengimpor langsung dari modul tetangga — dan dalam enam bulan kalian punya big ball of mud berpakaian struktur folder rapi. Tugas architect adalah membuat boundary itu nyata, terukur, dan dipaksakan tooling.
Cohesion mengukur seberapa erat anggota-anggota satu modul saling berkaitan. Modul cohesive punya satu tujuan; semua kode di dalamnya melayani tujuan itu.
| Tingkat | Ciri | Contoh |
|---|---|---|
| Tinggi (baik) | Semua elemen melayani satu konsep domain | ordering: cart, checkout, order state |
| Rendah (buruk) | Kumpulan utilitas tak bertuan | utils/ berisi helper string, format uang, kirim email |
Modul low-cohesion adalah tanda pemilahan salah: kode ditempatkan berdasarkan jenis teknis ("semua helper di utils") alih-alih makna bisnis. Gejala lanjutannya — setiap fitur menyentuh banyak file acak, dan utils/index.ts menjadi titik parkir temporer yang permanen.
Uji cepat cohesion: bisakah kalian menjelaskan isi modul dalam satu kalimat tanpa kata "dan" lebih dari sekali? Jika tidak, ia kemungkinan dua modul.
Coupling mengukur seberapa kuat satu modul bergantung pada detail modul lain. Tujuannya bukan nol (mustahil), melainkan coupling ke hal yang stabil: interface publik, bukan implementasi internal.
Jenis coupling dari paling buruk ke paling ringan:
Content → menyentuh internal langsung (terburuk)
Common → semua modul bergantung pada satu shared blob
Temporal → harus dieksekusi urut waktu bersama
Data → berbagi skema/tabel data
Control → satu modul mengendalikan alur modul lain
Interface → hanya via kontrak publik (terbaik)Tiga pelanggaran yang paling sering ditemui:
inventory_items langsung. Skema inventory berubah → ordering ikut rusak. Ini coupling data paling licik karena tidak terlihat di import statement.common yang diimpor semua modul. Setiap perubahan kecil memicu rebuild dan risiko lintas sistem.Modul yang benar punya dua zona:
module/ordering/
├── public/ ← SATU-satunya pintu masuk
│ ├── api.ts (fungsi & type publik)
│ └── events.ts (event yang dipancarkan/didengarkan)
└── internal/
├── domain/ ← larangan diimpor dari luar
├── application/
└── infrastructure/Aturan mainnya sederhana namun ketat:
public/.Dengan lima bounded context dari episode 4, struktur studi kasus kita menjadi:
modules:
catalog: { public_api: [getProduct, listProducts], emits: [ProductAdded] }
inventory: { public_api: [reserveStock, releaseStock], emits: [StockChanged] }
ordering:
public_api: [createOrder, cancelOrder]
consumes: [PaymentReceived, StockReserved]
payment: { public_api: [charge], emits: [PaymentReceived] }
shipping: { public_api: [scheduleDelivery], consumes: [OrderPaid] }
forbidden:
- "import dari folder internal modul lain"
- "query SQL ke tabel milik modul lain"
- "membagikan entity ORM antar modul"Peta ini bukan dokumen hiasan — ia bahan baku fitness function di bagian akhir episode.
Selain batas antar modul, atur juga arah dependency:
Siklus dependency adalah penyakit yang diam-diam: kompilasi tetap sukses, test tetap hijau, tetapi setiap refactor terasa seperti membongkar jaring laba-laba. Deteksi dini = murah.
Tip
Gunakan ESLint boundaries atau TS project references untuk melarang import lintas zona. Aturan yang hanya ada di dokumen wiki akan dilanggar saat deadline; aturan yang gagal di CI akan dipatuhi bahasa saat deadline — justru di sanalah kalian butuhnya dipatuhi.
Fitness function adalah pemeriksaan otomatis yang mengukur kesesuaian arsitektur dengan aturan yang kalian tetapkan — topik lengkapnya di episode 22. Untuk sekarang, mari buat versi paling dasar: script yang memastikan tidak ada impor dari internal modul lain:
import { globSync } from "node:fs"
const modulesDir = "src/modules"
const violations: string[] = []
for (const file of globSync(`${modulesDir}/**/*.ts`)) {
const owner = file.split("/")[2]
const imports = readImports(file)
for (const target of imports) {
if (!target.startsWith(modulesDir)) continue
const targetModule = target.split("/")[2]
if (targetModule !== owner) {
const isPublicApi = target.includes("/public/")
if (!isPublicApi) {
violations.push(
`${file} -> ${target} (bukan public API)`
)
}
}
}
}
if (violations.length > 0) {
console.error("PELANGGARAN BOUNDARY:\n" + violations.join("\n"))
process.exit(1)
}
console.log("Boundary check: OK")Pasang sebagai langkah CI. Dari hari pertama pelanggaran ditolak otomatis, diskusi "boleh nggak sih import sebentar" berhenti — jawabannya sudah dikodekan.
Pola yang sama bisa diperluas: cek tidak ada string SQL ke tabel modul lain (grep nama tabel per modul), cek event yang dikonsumsi memang ada di daftar events.ts, dan hitung fan-out tiap moduk untuk mendeteksi coupling berlebih sebelum jadi krisis.
controllers, services, repositories global. Semua fitur menyentuh semua folder; cohesion rendah struktural.shared berisi entity, DTO, dan helper yang dipakai semua orang. Ia menjadi coupling point terbesar sistem.Inti yang harus dibawa pulang:
Di episode 8 selanjutnya kita bahas data architecture — pemodelan data lintas context, konsistensi strong vs eventual, kepemilikan data antar modul, dan pola-pola distribusi data seperti outbox yang menjadi fondasi integrasi modul kita. Sampai jumpa!