← Semua pembelajaran / Go Nol → Enterprise
Fase 11 · Enterprise & Capstone

Monolit modular — dan kenapa biasanya bukan microservice

Batas modul yang jelas adalah tujuan yang benar. Microservice adalah salah satu cara mencapainya — dan hampir selalu bukan cara termurah, apalagi untuk tim yang belum punya batas yang jelas di dalam satu proses.

Sumber asli go.dev Resmi Rangkuman ~8 menit baca

Intisari

  • Ambil batas modulnya dulu di dalam satu binari; memisahkan proses bisa menyusul kalau memang perlu.
  • Go menegakkan sebagian batas secara gratis: internal/ dan larangan impor melingkar.
  • Uji arsitektur mengubah aturan "modul A tidak boleh mengimpor B" jadi kegagalan CI.
  • Microservice menukar pemanggilan fungsi yang pasti berhasil dengan panggilan jaringan yang bisa gagal — dan transaksi lintas modul hilang.
  • Pisahkan layanan saat ada alasan operasional: profil skala berbeda, batas kepatuhan, atau tim yang benar-benar terpisah.

Bentuk monolit modular

toko/
├── cmd/
│   ├── server/          # satu binari HTTP
│   └── worker/          # satu binari worker
└── internal/
    ├── platform/        # db, redis, otel, logger — dipakai semua modul
    ├── katalog/         # ← modul
    │   ├── katalog.go       tipe domain + aturan
    │   ├── layanan.go       API PUBLIK modul ini
    │   ├── penyimpan.go     interface yang dibutuhkannya
    │   ├── postgres.go      implementasi
    │   └── internal/        ← benar-benar privat, bahkan untuk modul lain
    ├── pesanan/
    ├── pembayaran/
    └── http/            # transport: merakit rute dari semua modul

internal/ bersarang adalah alat yang kurang dikenal. internal/katalog/internal/harga hanya bisa diimpor dari dalam internal/katalog/ — modul pesanan tidak bisa menyentuhnya sama sekali, dan compiler yang menegakkannya. Ini memberi enkapsulasi sungguhan di tingkat modul, tanpa memisahkan proses.

Aturan antar modul

// ✅ Modul berkomunikasi lewat API publiknya saja.
// internal/pesanan/layanan.go
type Katalog interface {
	Ambil(ctx context.Context, id int64) (katalog.Produk, error)
}

// ❌ JANGAN: pesanan menyentuh tabel milik katalog
//    SELECT * FROM produk WHERE ...   ← batas modul dilanggar diam-diam
AturanDitegakkan oleh
Modul tidak mengimpor internal/ modul lainCompiler
Tidak ada impor melingkarCompiler
Domain tidak mengimpor net/http/database/sqldepguard (Fase 8)
Modul tidak membaca tabel modul lainReview + konvensi penamaan tabel
Modul hanya dipanggil lewat LayananUji arsitektur
// Uji arsitektur: aturan jadi tes yang gagal, bukan dokumen yang dilanggar.
func TestBatasModul(t *testing.T) {
	larangan := map[string][]string{
		"internal/katalog":    {"internal/pesanan", "internal/pembayaran"},
		"internal/pembayaran": {"internal/katalog"},
	}

	for modul, terlarang := range larangan {
		out, err := exec.Command("go", "list", "-deps", "./"+modul+"/...").Output()
		if err != nil {
			t.Fatal(err)
		}
		for _, t2 := range terlarang {
			if strings.Contains(string(out), t2) {
				t.Errorf("%s mengimpor %s — batas modul dilanggar", modul, t2)
			}
		}
	}
}

Ongkos microservice yang jarang dihitung

Di monolitDi microservice
Panggilan fungsi — pasti berhasilPanggilan jaringan — bisa timeout, bisa gagal separuh
Transaksi database lintas modulTidak ada. Butuh saga, kompensasi, konsistensi akhir
Refactor lintas modul: satu commitBeberapa repo, beberapa deploy, terurut
Debug: satu stack traceTrace terdistribusi, wajib
Lingkungan lokal: go runDocker Compose dengan banyak layanan
Satu pipeline deploySatu per layanan; versi harus kompatibel
Ubah kontrak: compiler menolakUbah kontrak: gagal saat runtime, di produksi

Baris kedua adalah yang paling mahal dan paling sering diremehkan. "Buat pesanan lalu kurangi stok" adalah satu transaksi di monolit — atomik, gratis. Setelah dipisah jadi dua layanan, ia butuh saga dengan langkah kompensasi, penanganan kegagalan parsial, dan cara memperbaiki data saat kompensasi itu sendiri gagal. Semua itu kode yang harus ditulis, diuji, dan dirawat — untuk mendapatkan hasil yang sebelumnya sudah benar.

Kapan memisahkan benar-benar tepat

AlasanSah?
Profil skala berbeda ekstrem (pemrosesan video vs API)✅ Ya
Batas kepatuhan (data kartu terisolasi)✅ Ya
Tim benar-benar terpisah dengan siklus rilis sendiri✅ Ya
Kebutuhan teknologi berbeda (ML dengan Python)✅ Ya
Satu bagian butuh isolasi kegagalan yang kuat✅ Ya
"Kodenya sudah terlalu besar"❌ Perbaiki batas modul dulu
"Supaya lebih mudah diskalakan"❌ Tambah task lebih murah (Fase 9)
"Karena perusahaan besar melakukannya"❌ Mereka punya tim platform

Kabar baiknya: monolit modular yang benar membuat pemisahan jadi murah kalau nanti benar-benar dibutuhkan. Kalau modul pembayaran hanya dipanggil lewat interface Layanan dan tidak menyentuh tabel modul lain, memindahkannya ke proses sendiri berarti mengganti implementasi interface itu dengan klien HTTP atau gRPC — bukan menulis ulang aplikasi. Urutannya: batas dulu, proses belakangan.

Satu binari, beberapa peran

Satu image Docker, tiga service ECS (Fase 10):

  toko-server   command: ["/server"]     4 task, di belakang ALB
  toko-worker   command: ["/worker"]     2 task, tanpa ALB
  toko-cron     command: ["/cron"]       1 task

Yang didapat tanpa memisahkan basis kode:
  • skala per peran secara independen
  • worker yang mati tidak menjatuhkan API
  • satu versi, satu commit, satu pipeline

Latihan: pilih dua modul di aplikasimu dan tulis uji arsitektur yang melarang salah satunya mengimpor yang lain. Jalankan dan lihat apakah kodemu sudah sebersih yang kamu kira. Lalu pindahkan satu kelompok tipe yang benar-benar privat ke internal/<modul>/internal/ dan buktikan modul lain tidak bisa mengimpornya.

Rangkuman ini sengaja dipangkas ke bagian yang dipakai di roadmap. Buka sumber aslinya saat kamu butuh detail lengkap atau referensi parameter.