Realitas Produksi: Mengapa Modular Monolith Menjadi Pilihan Pragmatis
Latihan algoritma dan arsitektur dalam lingkungan sandbox sering mengabaikan gesekan fisik infrastruktur nyata. Di lingkungan produksi, komunikasi antar-layanan terikat oleh batas latensi jaringan, kegagalan serialisasi JSON atau Protobuf, dan ketidakpastian distributed state. Mengadopsi microservices terlalu dini memindahkan kompleksitas logika aplikasi ke lapisan jaringan dan infrastruktur.
Modular monolith mempertahankan batas logis domain bisnis tanpa membayar pajak jaringan (network tax). Modul tetap independen secara konseptual, tetapi eksekusi kode berjalan dalam satu ruang memori (in-memory) dan satu runtime proses.
Analisis Trade-off Biaya Cloud dan Infrastruktur
1. Compute dan Densitas Resource
Microservices membutuhkan alokasi resource minimum per service untuk menjamin high availability (minimal dua replika per node untuk redundansi). Jika Anda memiliki 20 microservices, Anda menjalankan minimal 40 kontainer dengan overhead runtime bahasa pemrograman, memory footprint dasar, dan agent monitoring di setiap pod. Pendekatan ini menghasilkan pemborosan resource akibat fragmentasi CPU dan RAM (bin-packing inefficiency).
Modular monolith mengeksekusi semua modul dalam pool komputasi terpadu. Utilisasi CPU dan RAM lebih padat dan efisien, mengurangi kebutuhan instance VM berlebih di cluster Kubernetes.
2. Latensi I/O dan Network Egress
Panggilan fungsi lokal antar-modul membutuhkan waktu hitungan nanodetik hingga mikrodetik. Mengubah panggilan ini menjadi panggilan HTTP/REST atau gRPC antar-service menaikkan latensi menjadi 2 hingga 15 milidetik per lompatan (hop). Dalam topologi microservices yang dalam, satu request pengguna dapat memicu puluhan sub-request.
Pemisahan service lintas availability zone (AZ) atau region juga memicu biaya cross-AZ data transfer (network egress) yang substansial pada tagihan cloud bulanan. Modular monolith mengeliminasi biaya transfer data internal ini karena seluruh eksekusi data terjadi via loopback memori internal.
3. Biaya Observabilitas dan Debugging
Microservices mewajibkan stack observabilitas terdistribusi: distributed tracing (seperti OpenTelemetry, Jaeger), log aggregation terpusat dengan volume tinggi, dan APM service-mesh. Biaya ingest log dan trace per gigabyte sering kali melampaui biaya komputasi aplikasi itu sendiri.
Modular monolith menyederhanakan debugging. Stack trace bersifat linier, korelasi request tersimpan dalam konteks thread atau goroutine lokal, dan observabilitas cukup dipenuhi melalui log terstruktur dan metrik aplikasi standar.
Beban Kognitif Tim dan Kompleksitas Deployment
Pemisahan service mendistribusikan basis kode, memicu overhead operasional tambahan:
- Versioning API dan Kontrak Data: Setiap perubahan skema membutuhkan backward compatibility bertingkat, deprecation strategy, dan contract testing (misal: Pact). Pada modular monolith, refactoring antarmuka modul diverifikasi langsung oleh compiler atau static type checker saat build.
- Pipeline CI/CD: Mengelola 30 pipeline deployment independen, integrasi rilis, dan dependensi antar-versi menambah beban tim Platform/DevOps. Modular monolith menggunakan satu pipeline deployment yang deterministik.
- Transaksionalitas: Modular monolith mengandalkan transaksi database ACID standar. Microservices memaksa adopsi pola konsistensi akhir (eventual consistency), outbox pattern, dan orkestrasi Saga yang rawan race condition.
Pola Penegakan Batas Modul dalam Satu Basis Kode
Tantangan terbesar modular monolith adalah mencegah degradasi menjadi spaghetti monolith. Kode antar-modul tidak boleh mengakses tabel atau dependensi internal domain lain secara langsung. Akses harus melewati antarmuka kontrak publik (API contract) modul.
Berikut contoh implementasi isolasi modul menggunakan Go dengan aturan dependensi eksplisit:
package inventory
import (
"context"
"errors"
"sync"
)
// DTO publik: data transfer object yang aman diekspos ke modul lain.
type ItemReservation struct {
SKU string
Quantity int
}
// Service mendefinisikan kontrak publik modul inventory.
type Service interface {
ReserveStock(ctx context.Context, reservation ItemReservation) error
}
type repository interface {
deductQuantity(ctx context.Context, sku string, qty int) error
}
type inventoryService struct {
repo repository
mu sync.Mutex
}
func NewService(repo repository) Service {
return &inventoryService{repo: repo}
}
func (s *inventoryService) ReserveStock(ctx context.Context, res ItemReservation) error {
s.mu.Lock()
defer s.mu.Unlock()
if res.Quantity <= 0 {
return errors.New("kuantitas reservasi tidak valid")
}
// Eksekusi mutasi stock via repository privat modul
return s.repo.deductQuantity(ctx context.Context, res.SKU, res.Quantity)
}
package order
import (
"context"
"fmt"
"project/modules/inventory"
)
type OrderUseCase struct {
// Modul order hanya bergantung pada antarmuka publik modul inventory.
// Akses langsung ke database atau struct internal inventory dilarang.
inventoryService inventory.Service
}
func NewOrderUseCase(inv inventory.Service) *OrderUseCase {
return &OrderUseCase{inventoryService: inv}
}
func (uc *OrderUseCase) Checkout(ctx context.Context, orderID string, sku string, qty int) error {
reservation := inventory.ItemReservation{
SKU: sku,
Quantity: qty,
}
if err := uc.inventoryService.ReserveStock(ctx context.Context, reservation); err != nil {
return fmt.Errorf("gagal memproses checkout %s: %w", orderID, err)
}
return nil
}
Pola di atas memastikan domain Order tidak memiliki akses langsung ke persistence layer milik Inventory. Jika modul Inventory nantinya dipisahkan menjadi service terpisah, representasi internal modul tetap utuh; adapter panggilan cukup dialihkan dari pemanggilan fungsi lokal ke klien RPC/HTTP.
Kriteria Kuantitatif untuk Ekstraksi Service
Pemisahan service tidak boleh didasarkan pada intuisi semata, melainkan metrik kuantitatif. Pisahkan modul menjadi service independen hanya jika memenuhi salah satu ambang batas berikut:
- Saturasi Resource yang Bersifat Asimetris: Salah satu modul membutuhkan profil hardware khusus (misal: encoding video butuh GPU, komputasi analitik butuh RAM gigantik) yang memaksa horizontal scaling seluruh monolith secara tidak ekonomis.
- Batas IOPS dan Koneksi Database Terkonsentrasi: Modul tertentu menghasilkan beban throughput I/O yang menghabiskan connection pool atau batas IOPS storage database utama, dan read-replica maupun sharding tabel tidak lagi mereduksi contention.
- Friksi Tim pada Pipeline CI/CD: Frekuensi commit dan durasi pipeline integrasi menyebabkan deployment queue lebih dari 45 menit, dengan lebih dari 25 engineer aktif melakukan merge ke trunk yang sama setiap hari.
- Kebutuhan Keamanan dan Kepatuhan Khusus: Modul memproses data terisolasi tinggi (misal: modul pemrosesan kartu kredit yang tunduk pada audit PCI-DSS level tinggi) di mana pemisahan fisik mengurangi cakupan audit sistem lain.
Jika sistem Anda melayani kurang dari puluhan ribu request per detik dan dikelola oleh tim rekayasa perangkat lunak di bawah 30 orang, arsitektur modular monolith hampir selalu menghasilkan throughput delivery fitur yang lebih tinggi dengan biaya operasional infrastruktur yang jauh lebih rendah.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!