L
REST API Design β€’ Literally Web ID

Desain RESTful API Berkinerja Tinggi dengan Pola CQRS dan Domain-Driven Design

⏱️ 15 menit baca β€’ πŸ“… 14 Oct 2026 β€’ πŸ‘οΈ 1 views
Desain RESTful API Berkinerja Tinggi dengan Pola CQRS dan Domain-Driven Design

1. Konteks Industri & Analisis Akar Masalah

Dalam lanskap pengembangan perangkat lunak enterprise modern, arsitektur RESTful API sering kali menjadi tumpuan komunikasi antar sistem, mulai dari aplikasi Single Page Application (SPA), mobile apps, hingga integrasi mikroservis lintas domain. Namun, seiring dengan pertumbuhan basis pengguna dan kompleksitas domain bisnis yang dikelola, pendekatan tradisional yang memadukan operasi baca (Reads) dan tulis (Writes) ke dalam satu model basis data tunggal (monolithic CRUD model) kerap mengalami degradasi performa yang parah.

Bottleneck performa utama dalam sistem berbasis CRUD konvensional berakar pada kontensi basis data (database contention). Ketika sebuah entitas bisnis yang kompleks harus dimodelkan ke dalam tabel relasional yang dinormalisasi secara ketat, operasi baca memerlukan JOIN yang masif, kompleks, dan memakan banyak sumber daya CPU serta RAM. Sebaliknya, operasi tulis memerlukan penguncian baris (row-level locking), validasi integritas transaksional (ACID), dan pemicu (triggers) yang memperlambat throughput sistem secara keseluruhan. Di lingkungan produksi dengan beban trafik tinggiβ€”misalnya ribuan requests per second (RPS)β€”kombinasi ini menciptakan deadlock, lonjakan latensi latensi pembacaan, dan pembengkakan penggunaan koneksi kolam basis data (connection pooling).

Sebelum penerapan CQRS (Command Query Responsibility Segregation) dan DDD (Domain-Driven Design), sebuah sistem biasanya mengalami kondisi Before: latensi tinggi pada endpoint GET akibat kalkulasi agregasi on-the-fly, kompleksitas logika bisnis yang tercampur aduk di dalam pengendali (controller) HTTP, serta kesulitan melakukan scaling independen karena basis data harus menanggung beban baca dan tulis secara bersamaan.

Sesudah penerapan (After), arsitektur dipisahkan secara tegas. Operasi tulis ditangani oleh Command Side yang dioptimalkan untuk konsistensi data melalui agregat DDD yang ketat, sementara operasi baca dilayani oleh Query Side yang memanfaatkan read models terdenormalisasi (disimpan dalam basis data dokumen atau cache memori tinggi seperti Redis). Hasilnya adalah peningkatan throughput hingga 300%, penurunan latensi rata-rata dari ratusan milidetik menjadi di bawah 20 milidetik, serta kemampuan untuk melakukan scaling infrastruktur baca dan tulis secara independen.

2. Konsep Inti & Deep Dive Arsitektur

CQRS pada dasarnya adalah pemisahan model yang digunakan untuk memperbarui data (Command) dari model yang digunakan untuk membaca data (Query). Dalam konteks Domain-Driven Design, pemisahan ini memperkuat batas kontekstual (Bounded Context) dan memungkinkan pemodelan domain yang sangat kaya tanpa mengorbankan performa pengambilan data.

[ Client / SPA / Mobile ]
          |
          +---> (POST/PUT/DELETE) ---> [ API Gateway / Nginx ] ---> [ Command Bus ] ---> [ Write Model (MySQL/PostgreSQL) ]
          |                                                                                              |
          +---> (GET) -------------> [ API Gateway / Nginx ] ---> [ Query Bus  ] ---> [ Read Model (Redis/ElasticSearch) ]

Di balik layar, alur eksekusi bekerja melalui dua jalur terpisah:

  1. Jalur Perintah (Command Pipeline): Klien mengirimkan permintaan mutasi (POST, PUT, DELETE). Permintaan ini diterima oleh API Gateway, diteruskan ke Command Handler, yang kemudian memuat Agregat DDD dari repositori tulis, menjalankan aturan bisnis internal, dan menyimpan perubahan ke basis data transaksional utama. Setelah transaksi berhasil, sebuah Domain Event dipublikasikan secara asinkron.
  2. Jalur Kueri (Query Pipeline): Klien mengirimkan permintaan baca (GET). Permintaan ini melewati Query Handler yang langsung membaca data dari Read Model yang telah dioptimalkan (bisa berupa tabel terdenormalisasi, Elasticsearch, atau Redis hash). Jalur ini sama sekali tidak menyentuh logika domain atau basis data transaksional.

[!IMPORTANT] Pemisahan ini memperkenalkan konsekuensi arsitektural berupa Eventual Consistency. Data yang baru saja ditulis melalui Command mungkin memerlukan beberapa milidetik sebelum tercermin di Read Model.

3. Perbandingan Komparatif / Analisis Pendekatan

Untuk memahami nilai tambah dari pendekatan ini, mari kita bandingkan secara objektif antara arsitektur Monolithic CRUD tradisional melawan arsitektur CQRS berbasis DDD.

Parameter Komparasi Monolithic CRUD Tradisional CQRS + DDD Berkinerja Tinggi
Kompleksitas Implementasi Rendah; mudah di awal, sulit di skala besar. Tinggi; membutuhkan pemahaman domain yang mendalam.
Penggunaan CPU & RAM (Read) Tinggi (melakukan JOIN kompleks pada data ter-normalisasi). Sangat Rendah (langsung mengambil data ter-denormalisasi).
Penggunaan CPU & RAM (Write) Tercampur dengan beban baca; rentan deadlock. Diisolasi; fokus penuh pada integritas transaksi domain.
Skalabilitas Infrastruktur Skala vertikal (harus memperbesar satu server basis data). Skala horizontal independen (bisa menambah Read Replicas).
Latensi Rata-rata (p99) > 250 ms pada beban tinggi. < 30 ms dengan strategi edge caching dan read model.
Maintainability (Jangka Panjang) Menurun drastis seiring bertambahnya logika di controller. Tinggi; domain terisolasi dengan jelas (Bounded Context).

4. Prasyarat Sistem & Persiapan Lingkungan

Sebelum membangun implementasi teknis, pastikan infrastruktur pendukung siap dan terkonfigurasi dengan benar.

  • Bahasa Pemrograman & Framework: Go (Golang) versi 1.21+ atau Node.js (TypeScript) dengan arsitektur bersih (Clean Architecture).
  • Basis Data Transaksional (Write): MySQL 8.0 / PostgreSQL 15 untuk menyimpan agregat domain dengan integritas ACID.
  • Basis Data Kueri (Read) & Caching: Redis 7.0+ untuk penyimpanan Read Models dan caching cepat.
  • Web Server & Reverse Proxy: Nginx sebagai pembatas rate limit dan terminasi SSL/TLS.
  • Dependency Sistem Operasi (Linux Ubuntu 22.04 LTS):
sudo apt update && sudo apt upgrade -y
sudo apt install -y build-essential git curl redis-server mysql-server nginx
sudo systemctl enable --now redis-server mysql

Verifikasi bahwa layanan berjalan normal:

sudo systemctl status redis-server mysql nginx

5. Panduan Implementasi Langkah demi Langkah

Berikut adalah implementasi praktis menggunakan Go (Golang) dengan struktur direktori yang menerapkan DDD dan CQRS.

Struktur Direktori Proyek

/project-root
β”œβ”€β”€ cmd/
β”‚   └── api/
β”‚       └── main.go
β”œβ”€β”€ internal/
β”‚   β”œβ”€β”€ domain/
β”‚   β”‚   └── user/
β”‚   β”‚       β”œβ”€β”€ aggregate.go
β”‚   β”‚       └── repository.go
β”‚   β”œβ”€β”€ application/
β”‚   β”‚   β”œβ”€β”€ commands/
β”‚   β”‚   β”‚   └── create_user.go
β”‚   β”‚   └── queries/
β”‚   β”‚       └── get_user.go
β”‚   └── infrastructure/
β”‚       β”œβ”€β”€ persistence/
β”‚       β”‚   β”œβ”€β”€ mysql_user_repo.go
β”‚       β”‚   └── redis_read_model.go
β”‚       └── http/
β”‚           └── router.go
└── go.mod

1. Domain Model (internal/domain/user/aggregate.go)

package user

import (
	"errors"
	"time"
)

type UserID string

type User struct {
	ID        UserID
	Email     string
	Name      string
	CreatedAt time.Time
}

func NewUser(id UserID, email, name string) (*User, error) {
	if email == "" {
		return nil, errors.New("email tidak boleh kosong")
	}
	return &User{
		ID:        id,
		Email:     email,
		Name:      name,
		CreatedAt: time.Now(),
	}, nil
}

2. Command Handler (internal/application/commands/create_user.go)

package commands

import (
	"context"
	"project-root/internal/domain/user"
)

type CreateUserCommand struct {
	ID    string
	Email string
	Name  string
}

type CreateUserHandler struct {
	WriteRepo user.Repository
	ReadCache user.ReadModelStore
}

func NewCreateUserHandler(writeRepo user.Repository, readCache user.ReadModelStore) *CreateUserHandler {
	return &CreateUserHandler{
		WriteRepo: writeRepo,
		ReadCache: readCache,
	}
}

func (h *CreateUserHandler) Handle(ctx context.Context, cmd CreateUserCommand) error {
	u, err := user.NewUser(user.UserID(cmd.ID), cmd.Email, cmd.Name)
	if err != nil {
		return err
	}

	// Simpan ke database tulis (MySQL)
	if err := h.WriteRepo.Save(ctx, u); err != nil {
		return err
	}

	// Perbarui read model secara langsung (atau via event bus)
	err = h.ReadCache.SetUserReadModel(ctx, u.ID, map[string]string{
		"email": u.Email,
		"name":  u.Name,
	})

	return err
}

3. Query Handler (internal/application/queries/get_user.go)

package queries

import (
	"context"
	"errors"
)

type GetUserQuery struct {
	ID string
}

type UserReadDTO struct {
	ID    string `json:"id"`
	Email string `json:"email"`
	Name  string `json:"name"`
}

type UserQueryService interface {
	GetByID(ctx context.Context, id string) (*UserReadDTO, error)
}

type GetUserHandler struct {
	QueryService UserQueryService
}

func NewGetUserHandler(qs UserQueryService) *GetUserHandler {
	return &GetUserHandler{QueryService: qs}
}

func (h *GetUserHandler) Handle(ctx context.Context, q GetUserQuery) (*UserReadDTO, error) {
	if q.ID == "" {
		return nil, errors.New("id tidak valid")
	}
	return h.QueryService.GetByID(ctx, q.ID)
}

4. Konfigurasi Nginx untuk Rate Limiting & Reverse Proxy (/etc/nginx/sites-available/api_gateway)

limit_req_zone $binary_remote_addr zone=api_limit:10m rate=50r/s;

server {
    listen 80;
    server_name api.enterprise.local;

    location / {
        limit_req zone=api_limit burst=20 nodelay;
        
        proxy_pass http://127.0.0.1:8080;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        # Optimasi Keepalive Connections
        proxy_http_version 1.1;
        proxy_set_header Connection "";
    }
}

Aktifkan konfigurasi Nginx:

sudo ln -s /etc/nginx/sites-available/api_gateway /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx

6. Bedah Kode & Penjelasan Detail Baris Kunci

  1. Pemisahan DTO pada Query Handler (UserReadDTO): Mengapa tidak menggunakan entitas Domain langsung untuk merespon API? Mengembalikan entitas Domain murni mengekspos struktur internal dan aturan bisnis. DTO (Data Transfer Object) pada lapisan kueri memastikan bahwa struktur JSON yang dikirimkan ke klien decoupled dari perubahan struktur basis data atau aturan domain internal.
  2. Pembaruan ReadCache di Command Handler: Baris h.ReadCache.SetUserReadModel(...) menunjukkan teknik Read Model Projections instan. Di sistem skala sangat besar, pembaruan ini dipublikasikan melalui Message Broker (seperti RabbitMQ atau Kafka). Namun, untuk efisiensi latensi rendah, pembaruan langsung ke Redis setelah commit transaksi tulis memberikan jaminan eventual consistency yang sangat cepat.
  3. Direktif Nginx proxy_http_version 1.1 dan Connection "": Pengaturan ini krusial untuk mengaktifkan HTTP Keep-Alive antara Nginx dan backend server. Tanpa ini, Nginx akan membuka dan menutup koneksi TCP untuk setiap permintaan HTTP, yang memicu lonjakan penggunaan CPU dan latensi yang tidak perlu pada trafik tinggi.

7. Skenario Produksi & Studi Kasus Dunia Nyata

Studi Kasus: Sistem E-Commerce Flash Sale yang melayani 50.000 Read Requests per detik dan 2.000 Write Requests (Checkout) per detik.

  • Pendekatan Lama: Basis data MySQL tunggal mengalami deadlock pada tabel pesanan dan inventaris. CPU mencapai 100%, dan latensi API melonjak di atas 3 detik.
  • Solusi Arsitektural:
    • Operasi Tulis (Checkout) diproses melalui Command Handler yang memvalidasi stok di Redis menggunakan transaksi atomik (Lua Scripting), kemudian menulis pesanan ke MySQL secara asinkron melalui antrean RabbitMQ.
    • Operasi Baca (Melihat Katalog & Status Pesanan) dilayani sepenuhnya oleh Query Handler yang membaca dari Redis Cluster dan dilindungi oleh Cloudflare Edge Caching dengan Cache-Control: public, max-age=60.
  • Hasil: Sistem mampu menangani lonjakan trafik tanpa kegagalan satupun, latensi p99 terjaga di bawah 15ms.

8. Common Pitfalls, Anti-Patterns & Solusi

  • Anti-Pattern 1: Menggunakan Basis Data Tulis untuk Kueri Kompleks. Dampak: Beban CPU melonjak, indeks basis data terfragmentasi, dan query lambat memengaruhi operasi tulis. Solusi: Selalu bangun Read Model terpisah untuk setiap kebutuhan kueri spesifik (Query-driven tables).
  • Anti-Pattern 2: Sinkronisasi Sinkron yang Berlebihan (Distributed Transactions / 2PC). Dampak: Menurunkan performa secara drastis dan rentan terhadap kegagalan jaringan. Solusi: Terapkan Eventual Consistency dengan pola Outbox Pattern dan pemrosesan asinkron.
  • Pitfall Keamanan: Mengabaikan Validasi Input di Command Layer. Dampak: Data korup masuk ke dalam domain agregat. Solusi: Terapkan validasi ketat di dalam fungsi pembangun agregat DDD (NewUser), bukan hanya di validasi HTTP middleware.

9. Benchmark, Metrik Kinerja & Tuning Performa

Untuk menguji performa API yang telah dibangun, gunakan tool pengujian beban modern seperti k6. Buat file skrip load-test.js:

import http from 'k6/http';
import { check, sleep } from 'k6';

export const options = {
  stages: [
    { duration: '30s', target: 100 },
    { duration: '1m', target: 500 },
    { duration: '30s', target: 0 },
  ],
};

export default function () {
  const res = http.get('http://api.enterprise.local/users/12345');
  check(res, {
    'status adalah 200': (r) => r.status === 200,
    'latensi di bawah 50ms': (r) => r.timings.duration < 50,
  });
  sleep(1);
}

Jalankan pengujian:

k6 run load-test.js

Tuning Kernel Linux (/etc/sysctl.conf) untuk Beban Tinggi:

# Meningkatkan batas deskriptor file
fs.file-max = 2097152

# Mengoptimalkan port ephemeral dan reuse koneksi TCP
net.ipv4.tcp_tw_reuse = 1
net.core.somaxconn = 65535
net.ipv4.ip_local_port_range = 1024 65535

Terapkan perubahan kernel:

sudo sysctl -p

10. Checklist Penerapan Produksi & Kesimpulan

Sebelum merilis arsitektur CQRS dan DDD ke lingkungan produksi, pastikan seluruh item dalam checklist berikut telah terpenuhi:

  • Pemisahan Write Model dan Read Model terimplementasi secara fisik di tingkat basis data.
  • Command Handler memvalidasi invarian domain dengan ketat.
  • Query Handler menggunakan DTO dan tidak mengekspos entitas domain mentah.
  • Nginx dikonfigurasi dengan rate limiting dan keepalive connections optimal.
  • Redis atau Read Store memiliki mekanisme failover / replikasi.
  • Pemantauan metrik (Prometheus & Grafana) aktif untuk mengawasi latensi p95/p99.

Dengan memadukan pola CQRS dan Domain-Driven Design, API enterprise Anda tidak hanya dirancang untuk berfungsi dengan benar secara fungsional, tetapi juga memiliki fondasi infrastruktur yang kokoh, terukur secara independen, dan siap menghadapi beban trafik ekstrem di masa depan.

🏷️ Tags: #Nginx #High Concurrency #Performance #Sysadmin #Web Server