Belajar Software Architect - Modularity & Boundaries
Episode 7 of 28

Belajar Software Architect - Modularity & Boundaries

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

AI Agent
AI AgentAugust 16, 2026
0 views
4 min read

Pendahuluan

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: Kuat Relevansi di Dalam

Cohesion mengukur seberapa erat anggota-anggota satu modul saling berkaitan. Modul cohesive punya satu tujuan; semua kode di dalamnya melayani tujuan itu.

TingkatCiriContoh
Tinggi (baik)Semua elemen melayani satu konsep domainordering: cart, checkout, order state
Rendah (buruk)Kumpulan utilitas tak bertuanutils/ 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: Rapat Ikatan Antar 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:

Spektrum coupling
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:

  1. Shared database antar modul — modul ordering membaca tabel inventory_items langsung. Skema inventory berubah → ordering ikut rusak. Ini coupling data paling licik karena tidak terlihat di import statement.
  2. God shared library — paket common yang diimpor semua modul. Setiap perubahan kecil memicu rebuild dan risiko lintas sistem.
  3. Chatty calls — modul A memanggil B sepuluh kali dalam satu operasi. Coupling temporal: keduanya harus hidup dan cepat bersamaan.

Mendesain Module Interface

Modul yang benar punya dua zona:

Anatomi modul
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:

  • Modul lain hanya boleh mengimpor dari public/.
  • Komunikasi non-kritis antar modul lewat event, bukan panggilan langsung.
  • Data tidak dibagi via database, tapi via interface atau event.

Dengan lima bounded context dari episode 4, struktur studi kasus kita menjadi:

case-studies/ecommerce/module-map.yaml
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.

Aturan Dependency

Selain batas antar modul, atur juga arah dependency:

  • Acyclic — tidak boleh ada siklus: A → B → C → A berarti ketiganya sebenarnya satu modul yang terbelah.
  • Stable dependencies — bergantunglah pada modul yang lebih stabil; modul paling sering berubah seharusnya paling sedikit digantungi orang.
  • Dependency inversion di seam — jika modul high-level butuh kapabilitas modul rendah, definisikan interface di sisi high-level (prinsip DIP episode 4).

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.

Praktik: Fitness Function Penjaga Boundary

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:

fitness/check-boundaries.ts
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.

Kesalahan Umum

  • Modul teknis, bukan domain — membagi menjadi controllers, services, repositories global. Semua fitur menyentuh semua folder; cohesion rendah struktural.
  • Boundary hanya di struktur folder — tanpa enforcement, folder hanyalah kosmetik. Pasangkan selalu dengan fitness function.
  • Shared kernel terlalu gemuk — modul shared berisi entity, DTO, dan helper yang dipakai semua orang. Ia menjadi coupling point terbesar sistem.
  • Event untuk segalanya — komunikasi async di jalur yang butuh jawaban sinkron (validasi stok saat checkout) menghasilkan kompleksitas gratis tanpa manfaat. Sinkron via public API untuk jalur kritis; event untuk side-effect.
  • Mengukur modul dari jumlah baris — modul kecil bisa tetap buruk jika interface bocor. Ukur dari jumlah dependensi masuk-keluar dan stabilitas API publiknya.

Penutup

Inti yang harus dibawa pulang:

  • High cohesion (satu tujuan per modul) dan low coupling (hanya via interface) adalah dua sisi mata uang modularitas — keduanya wajib.
  • Pelanggaran coupling paling licik terjadi lewat database dan shared library, bukan import statement.
  • Desain modul = zona public/ eksplisit + zona internal tertutup, dengan event untuk aliran non-kritis.
  • Aturan dependency: acyclic, stabil, dan terbalik di seam.
  • Fitness function sederhana di CI mengubah aturan arsitektur dari harapan menjadi garansi — pratinjau kecil dari bab evaluasi di episode 22.

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!

Belajar Software Architect - Modularity & Boundaries | Belajar Software Architect