Mengadopsi microservices terlalu dini membebani sistem dengan operational overhead yang tidak perlu: latensi serialisasi jaringan, orkestrasi cluster kompleks, dan konsumsi memori berlebih. Arsitektur Modular Monolith di Go Fiber menawarkan alternatif realistis: memisahkan domain secara logis di level kode tanpa membayar pajak infrastruktur komunikasi antar-layanan.

Efisiensi Biaya dan Komputasi Operasional

Go memiliki runtime yang sangat efisien, namun mendistribusikan sistem ke dalam puluhan microservices menimbulkan redundansi resource yang substansial:

  • Overhead Memori Runtime: Setiap microservice Go menjalankan instance runtime sendiri, termasuk scheduler goroutine, heap metadata, dan background garbage collector (GC). Sepuluh microservices minimal memakan baseline Resident Set Size (RSS) sepuluh kali lipat dibanding satu binary monolitik yang menangani seluruh domain.
  • Eliminasi Latensi I/O Jaringan: Komunikasi inter-service melalui gRPC atau REST/HTTP membutuhkan serialisasi (JSON/Protobuf marshaling), alokasi buffer jaringan, soket TCP, enkripsi TLS, dan kontekstual deadline. Modular monolith mengganti overhead ini dengan direct pointer passing di memori dengan alokasi heap mendekati nol.
  • Throughput Engine Fasthttp: Go Fiber dibangun di atas fasthttp yang menerapkan teknik zero memory allocation pada hot-path routing. Menggabungkan efisiensi Fiber dengan direct in-memory call menghasilkan respons API p99 jauh lebih konsisten dibanding agregasi API gateway ke downstream microservices.

Analisis Trade-Off Teknis

1. Direct Memory Call vs gRPC/HTTP Inter-Service

Pada microservices, kegagalan network call mengharuskan implementasi retries, backoff, circuit breaker, dan distributed tracing via OpenTelemetry. Pada modular monolith, pemanggilan antar-modul cukup berupa eksekusi interface Go reguler:

// Direct call: instan, aman secara tipe data pada compile time, zero serialization cost
isAvailable, err := m.inventoryModule.CheckStock(ctx, productID, qty)

Risikonya: tight coupling jika modularitas dilanggar via mutable shared state. Pointer yang dimodifikasi oleh modul penerima dapat memicu race condition jika dieksekusi secara asinkron tanpa proteksi mutex.

2. Shared Connection Pool vs Database-per-Service

Microservices mewajibkan pola database-per-service untuk isolasi data. Modular monolith umumnya mengandalkan satu database cluster bersama (shared database):

  • Keuntungan: Efisiensi connection pool. Daripada 20 microservices membuka masing-masing 10-20 pool koneksi ke PostgreSQL (menghabiskan batas max_connections database), satu binary Go Fiber hanya memerlukan satu pool terkelola via *sql.DB atau pgxpool.Pool. Transaksi ACID antar-tabel dapat dieksekusi dalam satu database transaction lokal tanpa koordinasi rumit seperti Saga Pattern.
  • Kelemahan: Risiko bypass isolasi domain. Engineer dapat dengan mudah menulis query SQL yang melakukan JOIN langsung ke tabel milik modul lain, merusak boundary domain secara permanen. Mitigasinya adalah isolasi schema PostgreSQL (misal schema order dan schema inventory) dengan database user terpisah jika diperlukan.

Implementasi Struktur Direktori Menggunakan Go Internal Package

Kompiler Go membatasi impor package di dalam direktori internal/ hanya untuk package induk langsungnya. Aturan ini memastikan modul lain tidak dapat mengimpor package private di dalam sub-modul.

my-fiber-app/
├── cmd/
│   └── api/
│       └── main.go
├── internal/
│   ├── inventory/
│   │   ├── handler.go
│   │   ├── repository.go
│   │   ├── service.go
│   │   └── module.go      // Public contract & interface untuk modul lain
│   └── order/
│       ├── handler.go
│       ├── repository.go
│       └── service.go
├── go.mod
└── go.sum

Definisi Contract Antar-Modul

Gunakan prinsip consumer-driven interface. Modul order mendefinisikan interface dependensinya sendiri, bukan mengimpor struct konkret dari inventory.

// internal/order/service.go
package order

import "context"

type StockChecker interface {
	CheckStock(ctx context.Context, productID string, quantity int) (bool, error)
}

type Service struct {
	stockChecker StockChecker
}

func NewService(sc StockChecker) *Service {
	return &Service{stockChecker: sc}
}

func (s *Service) CreateOrder(ctx context.Context, productID string, qty int) error {
	available, err := s.stockChecker.CheckStock(ctx, productID, qty)
	if err != nil || !available {
		return ErrStockUnavailable
	}
	// Proses order logic...
	return nil
}

Registrasi Route Fiber per Bounded Context

Setiap modul mengekspos fungsi untuk mendaftarkan endpoint HTTP-nya ke sub-router Fiber (fiber.Router):

// internal/order/handler.go
package order

import "github.com/gofiber/fiber/v2"

type Handler struct {
	service *Service
}

func NewHandler(s *Service) *Handler {
	return &Handler{service: s}
}

func (h *Handler) MountRoutes(router fiber.Router) {
	group := router.Group("/orders")
	group.Post("/", h.handleCreate)
}

func (h *Handler) handleCreate(c *fiber.Ctx) error {
	// Parsing & delegasi ke service
	return c.SendStatus(fiber.StatusCreated)
}

Wiring Dependency di Entry Point

Inisialisasi dilakukan terpusat pada cmd/api/main.go:

// cmd/api/main.go
package main

import (
	"log"
	"github.com/gofiber/fiber/v2"
	"my-fiber-app/internal/inventory"
	"my-fiber-app/internal/order"
)

func main() {
	app := fiber.New()
	api := app.Group("/api/v1")

	// Inisialisasi Modul Inventory
	inventoryService := inventory.NewService()
	inventoryHandler := inventory.NewHandler(inventoryService)
	inventoryHandler.MountRoutes(api)

	// Inisialisasi Modul Order (Inject inventoryService sebagai dependency)
	orderService := order.NewService(inventoryService)
	orderHandler := order.NewHandler(orderService)
	orderHandler.MountRoutes(api)

	log.Fatal(app.Listen(":3000"))
}

Batas Skala: Kapan Modul Harus Dipecah?

Pertahankan arsitektur Modular Monolith selama arsitektur ini mencukupi. Pertimbangkan ekstraksi modul menjadi independent microservice hanya saat kondisi berikut terpenuhi:

  1. Beban Komputasi Asimetris: Salah satu modul (misalnya modul pemrosesan video, image rendering, atau training ML kecil) membutuhkan resource CPU/GPU ekstrem yang memicu horizontal scaling terus-menerus. Memisahkan modul ini mencegah seluruh monolit ikut ter-scale tanpa kebutuhan riil.
  2. Skala Organisasi Tim: Skala tim developer melampaui 30-50 orang dan frekuensi deployment branch sering menghasilkan bottleneck merge conflict atau queue build CI/CD yang menghambat rilis harian.
  3. SLA dan Blast Radius Kritis: Kerusakan fatal (seperti runtime panic atau fatal memory leak) pada modul pelengkap tidak boleh menjatuhkan domain inti (core transactional engine).
  4. Kebutuhan Regulasi Data: Domain modul tertentu wajib mematuhi standar keamanan spesifik (seperti PCI-DSS atau HIPAA) yang memerlukan audit infrastruktur terisolasi secara ketat.