L
REST API Design • Literally Web ID

Desain REST API Skalabel dengan Go dan Clean Architecture untuk Enterprise

⏱️ 18 menit baca • 📅 14 Oct 2026 • 👁️ 1 views
Desain REST API Skalabel dengan Go dan Clean Architecture untuk Enterprise

Desain REST API Skalabel dengan Go dan Clean Architecture untuk Enterprise

1. Konteks Industri & Analisis Akar Masalah

Dalam lanskap pengembangan perangkat lunak enterprise modern, monolith terdistribusi dan layanan mikro (microservices) menuntut skalabilitas, maintainability, serta testability yang tinggi. Seiring bertambahnya kompleksitas bisnis, kode sumber (source code) aplikasi backend sering kali menjadi "Big Ball of Mud"—sebuah tumpukan kode di mana logika bisnis, skema basis data, dan penanganan protokol HTTP tercampur baur dalam satu lapisan.

Masalah Klasik Monolitik Terdistribusi:

  • Keterikatan Kuat (Tight Coupling): Perubahan pada skema basis data SQL memaksa perubahan masif pada pengontrol HTTP dan logika aplikasi inti.
  • Kesulitan Pengujian (Difficult Testing): Unit testing tidak dapat dilakukan tanpa menyalakan koneksi basis data atau memanggil layanan pihak ketiga.
  • Bottleneck Performa: Penggunaan framework web yang terlalu berat (bloated framework) di Go mengorbankan performa eksekusi konkurensi native yang ditawarkan oleh Go runtime.

Kondisi Sebelum vs Sesudah Penerapan

  • Sebelum Clean Architecture: Modifikasi fitur kecil membutuhkan waktu berhari-hari karena risiko side effect yang tinggi. Waktu kompilasi dan pengujian integrasi memakan waktu lama.
  • Sesudah Clean Architecture: Sistem terbagi secara tegas menjadi lapisan Domain, Use Case, dan Infrastructure. Pengujian unit dapat dieksekusi dalam hitungan milidetik menggunakan mocks. Pergantian basis data dari PostgreSQL ke MySQL tidak menyentuh logika bisnis sama sekali.

2. Konsep Inti & Deep Dive Arsitektur

Clean Architecture (diperopuler oleh Robert C. Martin / Uncle Bob) dan arsitektur Hexagonal (Ports and Adapters) menegaskan prinsip Dependency Inversion Principle (DIP). Lapisan terluar (Infrastructure) bergantung ke dalam (Domain), namun lapisan terdalam sama sekali tidak mengetahui keberadaan lapisan luar.

+-----------------------------------------------------------------------+
|                        INFRASTRUCTURE LAYER                           |
|  +-----------------------------------------------------------------+  |
|  |                        INTERFACE LAYER                          |  |
|  |  +-----------------------------------------------------------+  |  |
|  |  |                       USE CASE LAYER                      |  |  |
|  |  |  +-----------------------------------------------------+  |  |  |
|  |  |  |                     DOMAIN LAYER                    |  |  |  |
|  |  |  |  [Entities & Business Rules (Pure Go)]              |  |  |  |
|  |  |  +-----------------------------------------------------+  |  |  |
|  |  +-----------------------------------------------------------+  |  |
|  +-----------------------------------------------------------------+  |
+-----------------------------------------------------------------------+

Alur Eksekusi Data (Data Flow):

  1. HTTP Request diterima oleh Web Server (misalnya, Gin/Fiber atau net/http murni).
  2. Infrastructure Layer (Delivery/Handlers) memetakan payload HTTP JSON ke dalam objek DTO (Data Transfer Object).
  3. Use Case Layer menerima input, memanggil struktur Domain Entities, dan menerapkan aturan bisnis.
  4. Repository Pattern (Interface) dieksekusi untuk berkomunikasi dengan basis data atau caching layer (Redis).
  5. Hasil dikembalikan ke luar melalui pemetaan struktur data yang bersih.

3. Perbandingan Komparatif / Analisis Pendekatan

Parameter Pendekatan Monolitik Tradisional Clean Architecture dengan Go Pendekatan Berbasis ORM Berat
Latensi Eksekusi Sedang hingga Tinggi Sangat Rendah (Native Go) Tinggi (karena refleksi ORM)
Maintainability Buruk (Spaghetti Code) Sangat Tinggi (Modular) Sedang
Unit Test Coverage Sulit dicapai (< 40%) Mudah dicapai (> 90%) Sulit tanpa Mocking Database
Ketergantungan DB Sangat Kuat Diisolasi via Interface Sangat Kuat pada Objek ORM

[!WARNING] Jangan pernah membiarkan entitas Domain mengimpor pustaka dari Infrastructure Layer (seperti driver SQL spesifik atau framework HTTP). Hal ini melanggar aturan utama Clean Architecture.


4. Prasyarat Sistem & Persiapan Lingkungan

Sebelum mulai merancang kode, pastikan lingkungan pengembangan memenuhi spesifikasi standar enterprise:

  • Go Version: Go 1.22 atau versi terbaru.
  • Database: PostgreSQL 15+ / MySQL 8.0+
  • Caching & Queue: Redis 7+
  • Tooling: golangci-lint, sqlc (untuk type-safe SQL), dan migrate.

Inisialisasi modul Go:

mkdir enterprise-go-api
cd enterprise-go-api
go mod init github.com/enterprise/go-api

Struktur Direktori Proyek:

.├── cmd
│   └── server
│       └── main.go
├── internal
│   ├── domain
│   │   └── user.go
│   ├── usecase
│   │   └── user_usecase.go
│   ├── repository
│   │   └── postgres_user.go
│   └── delivery
│       └── http
│           ├── handler
│           │   └── user_handler.go
│           └── router.go
└── pkg
    ├── logger
    │   └── logger.go
    └── response
        └── json.go

5. Panduan Implementasi Langkah demi Langkah

Berikut adalah implementasi kode produksi lengkap tanpa placeholder.

A. Domain Layer (internal/domain/user.go)

package domain

import (
	"context"
	"time"
)

type User struct {
	ID        int64     `json:"id"`
	Name      string    `json:"name"`
	Email     string    `json:"email"`
	CreatedAt time.Time `json:"created_at"`
}

type UserRepository interface {
	GetByID(ctx context.Context, id int64) (*User, error)
	Create(ctx context.Context, user *User) error
}

type UserUseCase interface {
	GetUserProfile(ctx context.Context, id int64) (*User, error)
	RegisterUser(ctx context.Context, name, email string) (*User, error)
}

B. Use Case Layer (internal/usecase/user_usecase.go)

package usecase

import (
	"context"
	"time"
	"github.com/enterprise/go-api/internal/domain"
)

type userUseCase struct {
	userRepo       domain.UserRepository
	contextTimeout time.Duration
}

func NewUserUseCase(userRepo domain.UserRepository, timeout time.Duration) domain.UserUseCase {
	return &userUseCase{
		userRepo:       userRepo,
		contextTimeout: timeout,
	}
}

func (u *userUseCase) GetUserProfile(c context.Context, id int64) (*domain.User, error) {
	ctx, cancel := context.WithTimeout(c, u.contextTimeout)
	defer cancel()

	res, err := u.userRepo.GetByID(ctx, id)
	if err != nil {
		return nil, err
	}

	return res, nil
}

func (u *userUseCase) RegisterUser(c context.Context, name, email string) (*domain.User, error) {
	ctx, cancel := context.WithTimeout(c, u.contextTimeout)
	defer cancel()

	user := &domain.User{
		Name:      name,
		Email:     email,
		CreatedAt: time.Now(),
	}

	err := u.userRepo.Create(ctx, user)
	if err != nil {
		return nil, err
	}

	return user, nil
}

C. Infrastructure Repository (internal/repository/postgres_user.go)

package repository

import (
	"context"
	"database/sql"
	"github.com/enterprise/go-api/internal/domain"
)

type postgresUserRepository struct {
	Conn *sql.DB
}

func NewPostgresUserRepository(conn *sql.DB) domain.UserRepository {
	return &postgresUserRepository{Conn: conn}
}

func (p *postgresUserRepository) GetByID(ctx context.Context, id int64) (*domain.User, error) {
	query := `SELECT id, name, email, created_at FROM users WHERE id = $1`
	row := p.Conn.QueryRowContext(ctx, query, id)

	user := &domain.User{}
	err := row.Scan(&user.ID, &user.Name, &user.Email, &user.CreatedAt)
	if err != nil {
		if err == sql.ErrNoRows {
			return nil, domain.ErrNotFound
		}
		return nil, err
	}

	return user, nil
}

func (p *postgresUserRepository) Create(ctx context.Context, user *domain.User) error {
	query := `INSERT INTO users (name, email, created_at) VALUES ($1, $2, $3) RETURNING id`
	err := p.Conn.QueryRowContext(ctx, query, user.Name, user.Email, user.CreatedAt).Scan(&user.ID)
	return err
}

D. Delivery HTTP Handler (internal/delivery/http/handler/user_handler.go)

package handler

import (
	"net/http"
	"strconv"
	"github.com/enterprise/go-api/internal/domain"
	"github.com/gin-gonic/gin"
)

type UserHandler struct {
	UserUseCase domain.UserUseCase
}

func NewUserHandler(r *gin.Engine, us domain.UserUseCase) {
	handler := &UserHandler{UserUseCase: us}
	r.GET("/api/v1/users/:id", handler.GetByID)
	r.POST("/api/v1/users", handler.Create)
}

func (h *UserHandler) GetByID(c *gin.Context) {
	idParam := c.Param("id")
	id, err := strconv.ParseInt(idParam, 10, 64)
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": "invalid user id"})
		return
	}

	ctx := c.Request.Context()
	user, err := h.UserUseCase.GetUserProfile(ctx, id)
	if err != nil {
		c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
		return
	}

	c.JSON(http.StatusOK, user)
}

func (h *UserHandler) Create(c *gin.Context) {
	var req struct {
		Name  string `json:"name" binding:"required"`
		Email string `json:"email" binding:"required,email"`
	}

	if err := c.ShouldBindJSON(&req); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	ctx := c.Request.Context()
	user, err := h.UserUseCase.RegisterUser(ctx, req.Name, req.Email)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
		return
	}

	c.JSON(http.StatusCreated, user)
}

6. Bedah Kode & Penjelasan Detail Baris Kunci

  • Context Timeout pada Use Case (context.WithTimeout): Sangat krusial dalam lingkungan enterprise untuk mencegah goroutine leak dan menghabiskan koneksi basis data ketika klien memutus sambungan (disconnect) secara tiba-tiba.
  • Dependency Injection Manual: Mengirimkan interface ke dalam konstruktor (NewUserUseCase, NewPostgresUserRepository) memastikan fleksibilitas total saat pengujian tanpa memerlukan framework DI yang kompleks seperti Wire atau Dig jika tidak diperlukan.

7. Skenario Produksi & Studi Kasus Dunia Nyata

Pada beban lalu lintas tinggi (100.000+ RPS), pengelolaan koneksi database pool (sql.DB) wajib diatur secara ketat:

db.SetMaxOpenConns(100)
db.SetMaxIdleConns(10)
db.SetConnMaxLifetime(time.Hour)

Penggunaan Redis sebagai caching layer di depan PostgreSQL untuk endpoint GetByID akan memangkas latensi dari ~12ms menjadi <1.5ms.


8. Common Pitfalls, Anti-Patterns & Solusi

  • Anti-Pattern: Menggunakan database models langsung sebagai DTO response HTTP.
    • Solusi: Buat struktur DTO terpisah agar perubahan skema internal basis data tidak merusak kontrak API publik.
  • Pitfall: Mengabaikan penanganan error terpusat (Centralized Error Handling), mengakibatkan format respons error yang tidak konsisten di seluruh endpoint.

9. Benchmark, Metrik Kinerja & Tuning Performa

Uji performa menggunakan k6 atau wrk:

wrk -t12 -c400 -d30s http://localhost:8080/api/v1/users/1

Pastikan untuk memonitor metrik memori menggunakan pprof bawaan Go untuk mendeteksi alokasi heap yang berlebihan.


10. Checklist Penerapan Produksi & Kesimpulan

  • Semua error ditangani secara eksplisit tanpa mengabaikan err != nil.
  • Menggunakan connection pooling database yang optimal.
  • Menerapkan rate limiting dan middleware security (CORS, Helmet).
  • Logging terstruktur (JSON format menggunakan Zap atau Zerolog).
🏷️ Tags: #Golang #Backend #Rest Api #Clean Architecture #Enterprise