Pada aplikasi web konvensional dengan pola CRUD berbasis model tunggal, lonjakan lalu lintas pembacaan data (read-heavy) dapat membebani database utama (primary/writer). Operasi penulisan transaksional yang membutuhkan row-locking dan isolasi ketat kerap terhambat oleh kueri agregasi atau pencarian kompleks yang berjalan bersamaan. Dalam ekosistem Go, framework web Go Fiber menyediakan throughput HTTP tinggi berkat engine fasthttp. Namun, performa lapisan transport ini sering menjadi sia-sia jika lapisan persistensi mengalami connection starvation pada database relasional.
Command Query Responsibility Segregation (CQRS) memecah jalur baca (query) dan tulis (command) ke dalam model serta infrastruktur terpisah. Artikel ini menganalisis implementasi praktis CQRS di Go Fiber menggunakan read-replica database pool, evaluasi efisiensi konkurensi, serta trade-off teknis yang harus dikelola sebelum mengadopsinya ke tahap produksi.
Arsitektur: Monolit CRUD vs Segregasi Command-Query
Pola CRUD monolitik umumnya menggunakan satu koneksi database pool (*sql.DB atau *pgxpool.Pool) dan satu representasi struct domain/ORM untuk kedua operasi. Saat beban baca mencapai rasio ekstrem seperti 90:10 atau 99:1, pola ini memiliki batasan struktural:
- Model Coupling: Struct domain sarat dengan validasi bisnis dan relasi ORM yang kompleks, memperlambat proses serialisasi JSON saat melayani endpoint kueri sederhana.
- Pool Contention: Kueri
SELECTpanjang memakan kuota pool koneksi database, menyebabkan transaksiINSERT/UPDATEesensial mengantre (connection starvation).
Melalui CQRS tingkat persistensi, arsitektur dipecah menjadi dua alur yang berjalan independen:
- Command Path: Bertanggung jawab atas mutasi status (
POST,PUT,DELETE). Data divalidasi terhadap business rules ketat dan ditulis langsung ke Primary Database melalui transaksi relasional ACID. - Query Path: Bertanggung jawab atas penyajian data (
GET). Kueri dilempar langsung ke Read Replica Pool, memotong abstraksi domain/ORM untuk memetakan hasil kueri ke flat DTO siap saji.
Implementasi Segregasi DB Pool pada Handler Go Fiber
Pemisahan koneksi dapat diwujudkan tanpa memecah aplikasi menjadi microservices terdistribusi. Cukup buat dua pool database terpisah pada tahap inisialisasi aplikasi dan injeksikan ke handler Go Fiber yang sesuai.
package main
import (
"context"
"database/sql"
"log"
"time"
"github.com/gofiber/fiber/v2"
_ "github.com/jackc/pgx/v5/stdlib"
)
type DBCluster struct {
Primary *sql.DB // Write pool
Replica *sql.DB // Read pool
}
type ArticleCommandHandler struct {
db *sql.DB
}
type ArticleQueryHandler struct {
db *sql.DB
}
type CreateArticleCommand struct {
Title string `json:"title"`
Content string `json:"content"`
}
type ArticleDTO struct {
ID int64 `json:"id"`
Title string `json:"title"`
Content string `json:"content"`
CreatedAt time.Time `json:"created_at"`
}
func (h *ArticleCommandHandler) Create(c *fiber.Ctx) error {
var cmd CreateArticleCommand
if err := c.BodyParser(&cmd); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "payload tidak valid"})
}
// Boundary validasi domain
if len(cmd.Title) < 5 {
return c.Status(fiber.StatusUnprocessableEntity).JSON(fiber.Map{"error": "judul minimal 5 karakter"})
}
ctx, cancel := context.WithTimeout(c.Context(), 3*time.Second)
defer cancel()
var insertedID int64
query := `INSERT INTO articles (title, content, created_at) VALUES ($1, $2, NOW()) RETURNING id`
err := h.db.QueryRowContext(ctx, query, cmd.Title, cmd.Content).Scan(&insertedID)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "gagal menyimpan data"})
}
return c.Status(fiber.StatusCreated).JSON(fiber.Map{"id": insertedID})
}
func (h *ArticleQueryHandler) GetByID(c *fiber.Ctx) error {
id := c.Params("id")
ctx, cancel := context.WithTimeout(c.Context(), 2*time.Second)
defer cancel()
var dto ArticleDTO
// Query path: bypass domain overhead, scan langsung ke flat DTO
query := `SELECT id, title, content, created_at FROM articles WHERE id = $1`
err := h.db.QueryRowContext(ctx, query, id).Scan(&dto.ID, &dto.Title, &dto.Content, &dto.CreatedAt)
if err == sql.ErrNoRows {
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "artikel tidak ditemukan"})
} else if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "kueri gagal"})
}
return c.Status(fiber.StatusOK).JSON(dto)
}
func main() {
primaryDB, _ := sql.Open("pgx", "postgres://user:pass@primary-db:5432/appdb?sslmode=disable")
primaryDB.SetMaxOpenConns(25)
primaryDB.SetMaxIdleConns(10)
replicaDB, _ := sql.Open("pgx", "postgres://user:pass@replica-db:5432/appdb?sslmode=disable")
replicaDB.SetMaxOpenConns(100)
replicaDB.SetMaxIdleConns(50)
app := fiber.New()
cmdHandler := &ArticleCommandHandler{db: primaryDB}
queryHandler := &ArticleQueryHandler{db: replicaDB}
// Routing terpisah berdasarkan intent operasi
app.Post("/api/v1/articles", cmdHandler.Create)
app.Get("/api/v1/articles/:id", queryHandler.GetByID)
log.Fatal(app.Listen(":3000"))
}
Analisis Trade-Off Teknis
1. Eventual Consistency & Replication Lag
Konsekuensi penggunaan asynchronous replication dari primary ke replica database adalah adanya jeda waktu (replication lag). Masalah klasik muncul saat skenario read-your-own-writes:
Klien mengeksekusi
POST /api/v1/articles, menerima status 201 Created dengan ID 42, lalu browser otomatis mengeksekusiGET /api/v1/articles/42. Jika data belum terpropagasi ke replika, endpoint mengembalikan404 Not Found.
Mitigasi pada layer backend:
- Sticky Connection / Session Invalidation: Gunakan cookie atau token status untuk menandai bahwa pengguna baru saja melakukan mutasi. Rutekan kueri pengguna tersebut langsung ke Primary DB selama grace period singkat (misalnya 1-3 detik pasca-mutasi).
- Monotonic Read Verification: Pasang header LSN (Log Sequence Number) PostgreSQL atau versi transaksi. Jika LSN replika lebih rendah dari LSN terakhir klien, paksa kueri menunggu atau alihkan ke primary.
2. Boilerplate Kode vs Maintainability
Pemisahan model Command dan Query menuntut dua representasi data berbeda. Kebutuhan struct validasi, DTO kueri, serta ketiadaan ORM relasional pada alur kueri menaikkan jumlah baris kode hingga dua kali lipat dibanding CRUD standar. Jika domain bisnis sering berubah (tahap eksplorasi/MVP), perubahan skema mengharuskan maintenance simultan di kedua alur, meningkatkan risiko sinkronisasi model yang inkonsisten.
3. Efisiensi Runtime Fasthttp & Resource Footprint
Go Fiber dibangun di atas fasthttp yang berfokus pada zero-allocation memory melalui penggunaan pool buffer. Mengkombinasikan Fiber dengan query model CQRS menghasilkan efisiensi alokasi memori yang signifikan:
- Model kueri meniadakan pemuatan relasi yang tidak dibutuhkan (anti-N+1 dan pencegahan alokasi pointer bersarang dari ORM).
- Kueri baca dapat menggunakan proyeksi kolom SQL spesifik langsung ke memory stack atau buffer yang di-reuse.
- Hasil profil pprof menunjukkan penurunan drastis pada siklus kerja Garbage Collector (GC) Go karena siklus hidup struct DTO kueri bersifat flat dan lekas dialokasikan kembali.
4. Biaya Infrastruktur Basis Data
CQRS mempermudah vertical downscaling pada database utama. Primary DB hanya perlu menangani transaksi tulis tanpa beban kueri analitik berat, sehingga ukuran instans CPU/RAM dapat dipangkas. Sebaliknya, biaya infrastruktur dialihkan ke instans read replica yang dapat diskalakan secara horizontal (horizontal autoscaling). Biaya bandwidth transfer antar-zona (AZ) antardatabase replika menjadi variabel baru yang harus dihitung.
Panduan Keputusan: Kapan CQRS Layak Diadopsi?
Gunakan matriks keputusan berikut untuk menentukan kelayakan adopsi agar tidak terjebak dalam overengineering.
Lampu Hijau (Adopsi CQRS)
- Rasio baca-tulis mencapai 80:20 ke atas dan metrik database menunjukkan CPU bottleneck pada operasi baca sementara transaksi penulisan mengalami antrean lock.
- Struktur data yang dikonsumsi oleh UI/frontend sangat berbeda dari bentuk normalisasi data yang disimpan (misal: agregasi kompleks dari beberapa tabel).
- Kebutuhan bisnis mengizinkan konsistensi eventual (toleransi lag beberapa ratus milidetik).
Tanda Overengineering (Hindari CQRS)
- Aplikasi masih dalam fase MVP di mana model domain belum stabil dan skema database sering bermigrasi.
- Sistem menuntut strict immediate consistency secara absolut di seluruh fitur (misal: sistem saldo finansial/ledger yang tidak menoleransi pembacaan nilai usang).
- Volume query dapat ditangani cukup dengan menambahkan indeks database yang optimal, connection pooler (misal: PgBouncer), atau layer cache sederhana (misal: Redis).
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!