Skip to content

2. Application Patterns

Pola-pola Aplikasi / Application Patterns

Bab ini membahas pola-pola yang muncul saat membangun aplikasi modern: bagaimana memecah sistem jadi banyak layanan (microservice), bagaimana menangani operasi yang lama (async), bagaimana mengakses data dengan benar, kapan butuh cache, kapan butuh komunikasi event-driven, dan bagaimana memastikan operasi idempotent.

This chapter covers patterns that emerge when building modern applications: how to split a system into multiple services (microservice), how to handle long-running operations (async), how to access data correctly, when to use caching, when to use event-driven communication, and how to keep operations idempotent.


Microservice

Apa ini? / What is this?

ID: Microservice adalah pendekatan membangun aplikasi sebagai sekumpulan layanan kecil yang berdiri sendiri, masing-masing fokus pada satu kemampuan bisnis. Berbeda dengan monolith (satu aplikasi besar yang melakukan semuanya), microservice memecah sistem jadi banyak unit independen yang berkomunikasi lewat jaringan.

EN: Microservice is an approach to building applications as a collection of small, independent services, each focused on one business capability. Unlike a monolith (one big app doing everything), microservices split the system into many independent units that communicate over the network.

Analogi: pabrik mobil dengan banyak bengkel kecil (engine, body, electronics), masing-masing tahu pekerjaannya sendiri, lebih mudah memperbaiki/upgrade satu bengkel tanpa menghentikan seluruh pabrik.

Mengapa penting? / Why it matters?

  • Skala mandiri — bagian yang sibuk (mis. payment) bisa di-scale tanpa scale seluruh sistem.
  • Deployment independen — fix bug di service A tidak perlu redeploy service B.
  • Pemisahan tim — tim berbeda bisa pegang service berbeda tanpa saling mengganggu.
  • Teknologi heterogen — service A bisa pakai Python, service B pakai Go, tanpa konflik.

Tapi: microservice bukan obat mujarab. Kalau tim kecil & domain belum kompleks, monolith yang terstruktur sering lebih baik.

Use Case

Skenario: Aplikasi e-commerce dengan modul Catalog, Cart, Order, Payment, Shipping. Saat flash sale, traffic ke Catalog & Cart melonjak 50x lipat, tapi Payment tetap normal.

Dengan microservice: scale Catalog & Cart ke 50 pod, Payment tetap 3 pod. Hemat biaya infrastruktur.

Dengan monolith: harus scale seluruh aplikasi 50x lipat → 50x biaya, padahal sebagian besar pod idle.

Istilah & Konsep / Glossary

  • Stateless — service tidak menyimpan data di memorinya sendiri antar request. Jika butuh state, simpan di Redis / database eksternal. Dengan stateless, request bisa dilayani pod mana saja → mudah di-scale.

Contoh masalah stateful vs solusi stateless:

// ❌ SALAH: State tersimpan di memori service — hilang saat restart/scaling
public class CartService
{
    private readonly Dictionary<string, List<CartItem>> _carts = new();
    public void AddItem(string userId, CartItem item)
    {
        if (!_carts.ContainsKey(userId)) _carts[userId] = new List<CartItem>();
        _carts[userId].Add(item);
    }
}

// ✅ BENAR: State tersimpan di Redis (external shared store)
public class CartService
{
    private readonly IDistributedCache _cache;
    public async Task AddItemAsync(string userId, CartItem item)
    {
        var cacheKey = $"cart-service:cart:{userId}";
        var cart = await _cache.GetAsync<List<CartItem>>(cacheKey) ?? new List<CartItem>();
        cart.Add(item);
        await _cache.SetAsync(cacheKey, cart, TimeSpan.FromDays(7));
    }
}
flowchart LR
    U([User]) --> LB[Load Balancer]
    LB --> I1[Instance #1]
    LB --> I2[Instance #2]
    LB --> I3[Instance #3]
    I1 & I2 & I3 --> R[(Redis\nShared Store)]

    style R fill:#8E44AD,color:#fff
  • Bounded Context (dari DDD) — batas logis sebuah area bisnis. "Order" di konteks penjualan ≠ "Order" di konteks dapur restoran. Setiap microservice idealnya = satu bounded context.
  • Ubiquitous Language — istilah yang sama dipakai oleh developer, business analyst, dan dokumen. Tidak boleh kode menyebut "Cart" tapi dokumen bisnis menyebut "Basket".
  • Distributed Monolith — anti-pattern: punya banyak service tapi mereka saling terkait erat sehingga harus di-deploy bersamaan. Punya semua kerumitan microservice tanpa manfaatnya.
  • Saga — pola untuk mengelola transaksi yang melewati banyak service. Karena tidak ada database transaction lintas service, Saga membagi proses jadi langkah-langkah, masing-masing punya compensating action (langkah pembatalan).
  • Choreography vs Orchestration — dua gaya Saga: choreography = setiap service "mendengarkan" event dan bereaksi (tanpa kontrol pusat). Orchestration = ada satu service yang memimpin alur.
  • Circuit Breaker — pola seperti sekring listrik di rumah. Jika service downstream sering gagal, sirkuit "terbuka" → request langsung ditolak (tanpa menunggu timeout 30 detik) sampai service pulih.
  • 2PC (Two-Phase Commit) — protokol klasik untuk transaksi lintas database. Sangat rapuh di lingkungan terdistribusi dan jarang dipakai di microservice modern → diganti dengan Saga.
  • Rate Limiting — pembatas jumlah request per satuan waktu (mis. 100 req/menit per user). Mencegah abuse & melindungi service dari overload.
  • API Gateway — pintu masuk tunggal untuk semua request dari luar, sebelum diarahkan ke service yang tepat.

Anti-pattern

  • Database bersama antar service → mereka jadi kopel erat di level data, ubah skema = pecah banyak service.
  • Library bisnis bersama (mis. shared-business-logic.jar) → setiap update library memaksa redeploy semua service = distributed monolith.
  • Memanggil service lain di tengah transaksi DB → kalau service lain lambat, transaksi DB juga lambat → contention.
  • 2PC lintas service → rapuh, tidak skalabel.
  • Tidak ada retry / circuit breaker untuk panggilan eksternal → satu service lambat = semua service lambat (cascading failure).

Aturan / Rules

Mandatory

Mandatory Setiap service harus stateless. Jika butuh state, gunakan external store (Redis, DB).

Mandatory Setiap service memiliki data store-nya sendiri — tidak ada database bersama.

Mandatory Service berkomunikasi via REST/gRPC sinkron (untuk query) atau messaging asinkron (untuk command/event).

Sinkron (REST/gRPC) — untuk query yang butuh jawaban langsung:

sequenceDiagram
    participant Client
    participant OrderService
    participant ProductService

    Client->>OrderService: POST /orders (sync)
    OrderService->>ProductService: GET /products/123 (sync)
    ProductService-->>OrderService: product data
    OrderService-->>Client: order created response

Asinkron (Message Broker) — untuk command/event yang tidak butuh jawaban langsung:

sequenceDiagram
    participant OrderService
    participant MessageBroker
    participant NotificationService
    participant InventoryService

    OrderService->>MessageBroker: publish OrderCreatedEvent
    OrderService-->>Client: 202 Accepted (langsung)
    MessageBroker->>NotificationService: deliver event
    MessageBroker->>InventoryService: deliver event
    Note over NotificationService: kirim email konfirmasi
    Note over InventoryService: kurangi stok
Aspek REST/gRPC (Sinkron) Messaging (Asinkron)
Pola Query Command / Event
Coupling Temporal (caller menunggu) Decoupled temporal
Fault tolerance Rendah (cascade failure) Tinggi (consumer offline ≠ data hilang)
Contoh Cek harga produk Order confirmation email

Cara service berbagi data tanpa shared database:

// Opsi 1: API Call (query sinkron)
var product = await _productClient.GetProductAsync(request.ProductId);

// Opsi 2: Event-Driven (data denormalization)
// ProductService publish event → OrderService simpan salinan data lokal
public class ProductPriceUpdatedEventHandler : IEventHandler<ProductPriceUpdatedEvent>
{
    public async Task HandleAsync(ProductPriceUpdatedEvent @event)
    {
        await _localProductReadModel.UpdatePriceAsync(@event.ProductId, @event.NewPrice);
    }
}

Mandatory Batas service sejajar dengan bounded context — jangan pecah satu bounded context jadi banyak service.

Contoh bounded context yang berbeda untuk istilah yang sama:

Bounded Context: Katalog         Bounded Context: Inventory         Bounded Context: Pengiriman
┌─────────────────────┐          ┌─────────────────────┐            ┌─────────────────────┐
│ Produk:             │          │ Produk:             │            │ Produk:             │
│ - Nama              │          │ - SKU               │            │ - Berat             │
│ - Deskripsi         │          │ - Stok              │            │ - Dimensi           │
│ - Gambar            │          │ - Lokasi gudang     │            │ - Kategori bahaya   │
│ - Kategori          │          │ - Threshold reorder │            │ - Metode kemas      │
└─────────────────────┘          └─────────────────────┘            └─────────────────────┘

Anti-pattern: Split bounded context ke banyak service → CatalogService hanya simpan nama, PricingService simpan harga. Padahal nama & harga = satu konteks Katalog yang sama. Setiap menampilkan daftar produk harus panggil dua service → overhead.

Mandatory Hindari distributed monolith — service tidak berbagi library yang berisi logika bisnis.

Yang boleh dibagi (shared) vs yang tidak:

Boleh Dibagi (Shared) Tidak Boleh Dibagi
HTTP middleware (logging, auth header) Logika validasi bisnis
DTO/Contract untuk API publik Perhitungan harga/diskon
Utility umum (date helper, string extension) Domain entities dan aggregates
Konfigurasi observability (OpenTelemetry) Repository implementations

Mandatory Gunakan Ubiquitous Language secara konsisten dalam satu bounded context.

Mandatory Aggregate Root adalah satu-satunya pintu untuk mengubah state aggregate.

Mandatory Komunikasi lintas bounded context lewat Domain Event, bukan kopel langsung.

Mandatory Setiap service bisa di-deploy secara independen tanpa rilis terkoordinasi.

Optional — Distributed Transaction (Saga)

Optional JANGAN pakai 2PC lintas service — pakai Saga sebagai gantinya.

Optional Pilih gaya Saga berdasarkan kompleksitas: Choreography (sederhana, event-based) atau Orchestration (mudah dilacak, ada koordinator pusat).

Optional Setiap langkah Saga punya compensating transaction untuk rollback.

Optional Saga state harus persisten — bisa bertahan setelah restart proses.

Optional Desain untuk partial failure — sistem selalu bisa mencapai state konsisten akhirnya.

Optional — Circuit Breaker & Retry

Optional Bungkus semua outbound call (HTTP, gRPC, DB, broker) dengan circuit breaker.

Optional Status circuit breaker: Closed (normal), Open (gagal — tolak segera), Half-Open (uji pemulihan).

Optional Retry: maksimal 3x dengan exponential backoff + random jitter.

Optional JANGAN retry untuk: error 4xx, business rule violation, atau operasi non-idempotent.

Optional Log setiap perubahan status circuit breaker sebagai warning.

Optional Ekspos status circuit breaker via metrics.

Optional — Rate Limiting & Throttling

Optional Terapkan rate limiting di API Gateway untuk semua endpoint publik.

Optional Atur limit per: user, tenant, IP, dan global per endpoint.

Optional Saat limit terlampaui, kembalikan HTTP 429 Too Many Requests dengan header Retry-After.

Optional Whitelist panggilan internal antar service dari rate limit user-facing.

Optional Log pelanggaran rate limit untuk monitoring abuse.

Contoh Struktur / Example

Folder / File Deskripsi
src/ Root kode aplikasi
├── shared/contracts/ Event schema, DTO antar service
├── shared/common/ Logger, http-client wrapper (NO business logic!)
└── service-name/ Folder per service / bounded context
├── src/presentation/ REST / gRPC controller
├── src/application/ Use case, orchestrator
├── src/domain/ Entity, Value Object, Repository interface
├── src/infrastructure/ DB, HTTP client, queue implementation
├── tests/unit/ Unit test (no I/O)
├── tests/integration/ Integration test dengan DB / queue nyata
├── tests/contract/ Consumer-driven contract test
└── Dockerfile Container image definition per service
infra/ Konfigurasi infrastruktur
docker-compose.yml Definisi container untuk local development

Async & Concurrency

Apa ini? / What is this?

ID: "Async" (asynchronous) artinya operasi tidak langsung mengembalikan hasil — kita melepaskannya untuk dikerjakan di belakang sambil program lanjut melakukan hal lain. "Concurrency" artinya banyak hal berjalan "bersamaan" — bisa di banyak thread (multi-threading) atau di banyak CPU/core (parallelism).

EN: "Async" means an operation doesn't return a result immediately — we hand it off to be done in the background while the program continues. "Concurrency" means many things run "simultaneously" — either across many threads (multi-threading) or many CPUs/cores (parallelism).

Analogi: di restoran, koki tidak menunggu nasi matang baru goreng ayam — ia mulai keduanya bersamaan (concurrency). Sementara itu pelayan tidak menunggu satu meja selesai makan baru melayani meja lain (async).

Mengapa penting? / Why it matters?

  • Pengalaman pengguna — request HTTP yang butuh 30 detik (mis. generate laporan PDF) tidak boleh memblokir browser.
  • Throughput — server bisa melayani lebih banyak request bila tidak menunggu I/O.
  • Tahan banting — pekerjaan asinkron dengan queue tidak hilang meskipun server restart.

Use Case

Skenario 1 (Async): User klik "Generate Laporan Tahunan". Tanpa async, browser menunggu 2 menit dan timeout. Dengan async: server taruh task di queue → langsung kembalikan "Laporan sedang diproses, akan dikirim ke email Anda" → user lanjut bekerja.

Skenario 2 (Multi-threading): Server menerima 1000 request paralel untuk fetch data. Jika single-thread, request ke-1000 menunggu 999 request selesai. Dengan thread pool ukuran 100, sampai 100 request bisa diproses bersamaan.

Skenario 3 (Parallelism): Anda memproses 10 juta record. Single-core butuh 1 jam. Dengan partitioning + 10 worker paralel di 10 core, selesai dalam 6 menit.

Istilah & Konsep / Glossary

  • Thread Pool — sekumpulan thread yang sudah disiapkan, dipakai berulang. Lebih efisien daripada membuat thread baru setiap task.
  • I/O-bound vs CPU-bound — task yang menunggu disk/network = I/O-bound (perlu banyak thread, CPU idle). Task hitung-hitungan berat = CPU-bound (jumlah thread ≤ core, kalau lebih malah pelan).
  • Dead-letter Queue (DLQ) — antrian khusus untuk pesan yang gagal diproses beberapa kali. Diperiksa manual/otomatis untuk investigasi.
  • Race Condition — bug ketika dua thread mengakses data yang sama tanpa sinkronisasi, hasilnya bergantung "siapa duluan" — sulit di-debug.
  • Deadlock — dua thread saling menunggu lock yang dipegang yang lain → keduanya berhenti selamanya.
  • Cancellation Token — mekanisme untuk membatalkan operasi async (mis. user pencet "Cancel").
  • Backpressure — saat consumer lebih lambat dari producer, sistem perlu mekanisme untuk "menahan" producer agar tidak overload.

Anti-pattern

  • ❌ Blok thread HTTP server untuk operasi yang lama → throughput jatuh, request lain antri.
  • ❌ Membuat thread tanpa batas (new Thread() di setiap request) → OutOfMemory.
  • ❌ Tidak menangani error di task async → silently swallowed, bug tidak terdeteksi.
  • ❌ Lock bersarang dengan urutan tidak konsisten → deadlock.
  • ❌ Asumsi "parallel pasti lebih cepat" — overhead konteks switch & koordinasi bisa lebih besar dari gain.

Aturan / Rules

Mandatory — Async

Mandatory Operasi yang tidak butuh hasil sinkron harus diproses async.

Mandatory Task lama dioffload ke background worker / message queue — jangan blokir request thread.

Mandatory Operasi async harus observable — punya endpoint status atau emit event untuk completion & failure.

Mandatory Selalu handle failure async secara eksplisit — dead-letter queue wajib dikonfigurasi.

Mandatory — Multi-threading

Mandatory Shared mutable state dilindungi mekanisme sinkronisasi yang sesuai.

Mandatory Lebih baik pakai struktur data immutable jika memungkinkan — hilangkan kebutuhan locking.

Mandatory Ukuran thread pool dikonfigurasi eksplisit — jangan andalkan default.

Mandatory Hindari thread starvation: pisahkan thread pool untuk I/O-bound dan CPU-bound.

Mandatory Pola rawan deadlock (nested lock, urutan lock tidak konsisten) di-review di code review.

Mandatory — Parallelism

Mandatory Gunakan parallelism hanya jika operasi benar-benar independen dan overhead-nya sepadan.

Mandatory Partisi workload secara eksplisit — jangan andalkan implicit parallelism.

Mandatory Operasi paralel punya timeout & cancellation strategy yang jelas.

Mandatory Error di satu cabang paralel tidak boleh menelan error cabang lain — agregasi & surface semua failure.

Mandatory Lakukan benchmark sebelum-sesudah untuk memastikan parallelism benar-benar meningkatkan throughput.


Data Access

Apa ini? / What is this?

ID: Cara aplikasi membaca & menulis data ke penyimpanan (database, file, dll). Pola yang dipakai menentukan: kebersihan kode, performa, dan kemudahan testing.

EN: How the application reads & writes data to storage (database, files, etc.). The patterns used determine code cleanliness, performance, and testability.

Mengapa penting? / Why it matters?

  • Tanpa abstraksi, SQL/ORM tersebar di seluruh kode → ganti DB = pekerjaan masif.
  • Query tanpa pagination = bom waktu (saat data tumbuh, query menggantung server).
  • N+1 query = penyakit klasik yang membuat aplikasi pelan tanpa terlihat di awal.

Use Case

Skenario N+1: API /orders mengembalikan 100 order, masing-masing dengan list items. Kode naif: 1 query ambil 100 order, lalu 100 query lagi (satu per order) untuk ambil items → 101 query untuk 1 endpoint! Database overloaded saat traffic tinggi.

Solusi: eager loading atau batch fetching → 2 query saja (1 untuk orders, 1 untuk semua items di orders tersebut, lalu join di memory).

Istilah & Konsep / Glossary

  • Repository Pattern — abstraksi seperti "koleksi" entity di memori. Kode bisnis cukup orderRepo.findById(id), tidak tahu apakah belakangnya SQL, MongoDB, atau in-memory dictionary.
  • ORM (Object-Relational Mapping) — library yang memetakan tabel ke class (mis. Hibernate, Entity Framework, SQLAlchemy, Prisma).
  • Query Object — pola untuk merangkai filter query kompleks tanpa SQL hardcoded di service.
  • N+1 Problem — bug performa: 1 query parent + N query child = N+1 round-trip ke DB.
  • Connection Pool — kumpulan koneksi DB yang dipakai bergantian; jauh lebih cepat daripada buka-tutup koneksi tiap request.
  • CQRS (Command Query Responsibility Segregation) — pola memisahkan model & path untuk operasi write (command) dan read (query). Cocok kalau pola baca jauh berbeda dari pola tulis (mis. tulis transaksional, baca laporan analitik).
  • Pagination — membatasi jumlah data yang dikembalikan per request (mis. 50 baris per halaman). Dua pendekatan utama:
    • Offset-based (?page=2&pageSize=20) — mudah tapi tidak efisien untuk dataset besar.
    • Cursor-based / Keyset (?cursor=abc&pageSize=20) — performa konstan, cocok untuk data besar yang terus tumbuh.

Anti-pattern

  • ❌ Inline SQL string di controller / service: db.execute("SELECT * FROM users WHERE id=" + userId) → rawan SQL injection + sulit di-test.
  • findAll() tanpa pagination di endpoint API publik.
  • ❌ Connection di-open di setiap query tanpa pool.
  • ❌ Loop dalam loop yang memicu query DB di setiap iterasi.

Contoh N+1 Problem & Solusi

// ❌ N+1 PROBLEM — 101 query untuk 100 order!
var orders = await _context.Orders.ToListAsync();              // 1 query
foreach (var order in orders)
{
    var customer = await _context.Customers.FindAsync(order.CustomerId); // N query
    // ...
}

// ✅ SOLUSI 1: Eager Loading — satu query JOIN
var orders = await _context.Orders
    .Include(o => o.Customer)
    .Include(o => o.OrderItems).ThenInclude(oi => oi.Product)
    .ToListAsync();

// ✅ SOLUSI 2: Batch Fetching — 2 query total
var orders = await _context.Orders.ToListAsync();
var customerIds = orders.Select(o => o.CustomerId).Distinct().ToList();
var customers = await _context.Customers
    .Where(c => customerIds.Contains(c.Id))
    .ToDictionaryAsync(c => c.Id);

// ✅ SOLUSI 3: Projection — pilih kolom yang dibutuhkan saja
var result = await _context.Orders
    .Select(o => new OrderDto
    {
        OrderId = o.Id,
        CustomerName = o.Customer.Name,  // EF Core otomatis JOIN
        TotalAmount = o.TotalAmount
    }).ToListAsync();

Contoh Repository + Mock Test

// Domain layer — interface
public interface IProductRepository
{
    Task<Product> GetByIdAsync(ProductId id, CancellationToken ct = default);
    Task AddAsync(Product product, CancellationToken ct = default);
}

// Unit test — mock repository, tanpa database
[Fact]
public async Task CreateOrder_ShouldFail_WhenProductNotFound()
{
    var mockRepo = new Mock<IProductRepository>();
    mockRepo.Setup(r => r.GetByIdAsync(It.IsAny<ProductId>(), default))
        .ReturnsAsync((Product)null);

    var service = new OrderService(mockRepo.Object);
    await Assert.ThrowsAsync<ProductNotFoundException>(
        () => service.CreateOrderAsync(new CreateOrderRequest { ProductId = 99 }));
}

Contoh Cursor-Based Pagination

// Model
public class CursorPagedRequest
{
    public string? Cursor { get; set; }
    public int PageSize { get; set; } = 20;
}

// Repository — performa konstan meskipun dataset besar
public async Task<CursorPagedResult<Product>> GetProductsAsync(CursorPagedRequest request)
{
    var query = _context.Products.Where(p => p.IsActive);

    if (request.Cursor != null)
    {
        var lastId = int.Parse(request.Cursor);
        query = query.Where(p => p.Id > lastId);
    }

    var items = await query.OrderBy(p => p.Id)
        .Take(request.PageSize + 1).ToListAsync();

    var hasNextPage = items.Count > request.PageSize;
    if (hasNextPage) items = items.Take(request.PageSize).ToList();

    return new CursorPagedResult<Product>
    {
        Items = items,
        NextCursor = hasNextPage ? items.Last().Id.ToString() : null
    };
}

Contoh Connection Pool Configuration

// .NET — connection string dengan pool config eksplisit
"Server=localhost;Database=mydb;Min Pool Size=5;Max Pool Size=100;Connection Timeout=30;"
# Java (HikariCP / Spring Boot)
spring:
  datasource:
    hikari:
      minimum-idle: 5
      maximum-pool-size: 20
      connection-timeout: 30000
// Node.js (pg / node-postgres)
const pool = new Pool({ min: 5, max: 20, idleTimeoutMillis: 30000 });
Setting Panduan
min Baseline traffic normal (5–10)
max Jangan melebihi kemampuan DB server. PostgreSQL 4 core → max ~100
connectionTimeout 5–30 detik. Lebih dari ini → error ke client
idleTimeout Cegah akumulasi koneksi idle

Contoh Query Object Pattern

// Query object — merepresentasikan satu query dengan semua kriterianya
public class ProductQuery
{
    public int? CategoryId { get; set; }
    public decimal? MinPrice { get; set; }
    public decimal? MaxPrice { get; set; }
    public string? Keyword { get; set; }
    public int Page { get; set; } = 1;
    public int PageSize { get; set; } = 20;
}

// Repository — filter diterapkan kondisional
public async Task<PagedResult<Product>> SearchAsync(ProductQuery query, CancellationToken ct)
{
    var q = _context.Products.AsQueryable();
    if (query.CategoryId.HasValue) q = q.Where(p => p.CategoryId == query.CategoryId.Value);
    if (query.MinPrice.HasValue) q = q.Where(p => p.Price >= query.MinPrice.Value);
    if (!string.IsNullOrEmpty(query.Keyword)) q = q.Where(p => p.Name.Contains(query.Keyword));

    var totalCount = await q.CountAsync(ct);
    var items = await q.OrderBy(p => p.Name)
        .Skip((query.Page - 1) * query.PageSize).Take(query.PageSize).ToListAsync(ct);

    return new PagedResult<Product> { Items = items, TotalCount = totalCount };
}

// Penggunaan — ekspresif dan mudah dibaca
var results = await _repo.SearchAsync(new ProductQuery
    { CategoryId = 5, MinPrice = 50_000, Keyword = "laptop", Page = 1, PageSize = 20 });

Aturan / Rules

Mandatory

Mandatory Pakai Repository Pattern untuk semua akses data domain.

Mandatory Jangan tulis SQL mentah inline di logika bisnis — pakai ORM, query builder, atau stored procedure secara konsisten.

Mandatory Pagination wajib untuk semua list query — tidak boleh ada result set tanpa batas.

Mandatory Connection DB dikelola via connection pool dengan min/max yang eksplisit.

Mandatory Pakai Query Object untuk read yang kompleks.

Mandatory Hindari N+1 query — gunakan eager loading atau batch fetching.

Optional

Optional Pisahkan read model dari write model (CQRS) jika beban baca & tulis sangat berbeda.


Cache Management

Apa ini? / What is this?

ID: Cache adalah penyimpanan sementara yang lebih cepat diakses daripada sumber aslinya. Bayangkan menyimpan jawaban yang sering ditanya di sticky note di meja, daripada bolak-balik buka buku tebal.

EN: A cache is temporary storage that's faster to access than the original source. Like sticky notes on your desk with frequent answers, instead of opening a thick book every time.

Mengapa penting? / Why it matters?

  • Database sering jadi bottleneck — cache memindahkan beban ke memory.
  • Saat flash sale, halaman produk di-hit jutaan kali — tanpa cache, DB tewas.
  • Latensi turun drastis (mis. dari 80ms ke 2ms).

Use Case

Skenario flash sale: Halaman detail produk best-seller di-hit 100.000x/menit. Tanpa cache: 100.000 query identik ke DB → DB overload. Dengan cache TTL 60 detik: hanya ~1 query/menit ke DB (saat cache expire).

Istilah & Konsep / Glossary

  • TTL (Time-To-Live) — masa berlaku entry cache. Setelah TTL habis, entry dianggap kadaluwarsa.
  • Cache Hit / Misshit = data ada di cache. Miss = tidak ada, harus ambil dari sumber.
  • Cache-Aside (Lazy Loading) — aplikasi cek cache dulu; jika miss, baca dari DB & isi cache. Pola paling umum.
  • Write-Through — setiap tulis ke DB, sekaligus update cache.
  • Write-Behind — tulis ke cache dulu, async tulis ke DB.
  • Cache Invalidation — menghapus/mengganti entry cache saat data sumbernya berubah. Salah satu hardest problem in computer science.
  • Cache Stampede / Thundering Herd — saat cache satu key expire dan ribuan request bersamaan miss → semua hit DB sekaligus.
  • L1 / L2 Cache — L1: in-process (di memori aplikasi, paling cepat tapi tidak shared antar instance). L2: Redis / Memcached, shared antar instance.
  • Redis — in-memory data store yang umum dipakai untuk cache (dan banyak hal lain: queue, pub/sub).

Anti-pattern

  • ❌ Cache tanpa TTL → data basi selamanya.
  • ❌ Lupa invalidasi saat update → user lihat data lama.
  • ❌ Cache key tidak konsisten (user-123, users:123, User_123) → cache miss tanpa sadar.
  • ❌ Cache data sensitif (token, password) tanpa enkripsi → bocor jika Redis ter-dump.

Contoh Implementasi Cache / Cache Implementation Examples

Cache-Aside (Lazy Loading)

flowchart TD
    A([Client Request]) --> B[Check Cache]
    B --> C{Cache Hit?}
    C -- HIT --> D([Return Cached Data])
    C -- MISS --> E[Fetch from Database]
    E --> F[Store in Cache with TTL]
    F --> G([Return Data])

    style D fill:#27AE60,color:#fff,stroke:none
    style G fill:#27AE60,color:#fff,stroke:none
    style E fill:#E67E22,color:#fff,stroke:none
// Cache-Aside — pola paling umum
public async Task<Product> GetProductByIdAsync(int productId)
{
    var cacheKey = $"product-service:product:{productId}";
    var cached = await _cache.GetAsync<Product>(cacheKey);
    if (cached != null) return cached;

    var product = await _repository.GetByIdAsync(productId);
    await _cache.SetAsync(cacheKey, product, TimeSpan.FromHours(24));
    return product;
}

// Invalidate saat update
public async Task UpdateProductAsync(int productId, UpdateProductRequest request)
{
    await _repository.UpdateAsync(productId, request);
    await _cache.DeleteAsync($"product-service:product:{productId}");
}

Write-Through

// Write-Through — tulis ke DB dan cache sekaligus
public async Task<Product> CreateProductAsync(CreateProductRequest request)
{
    var product = await _repository.CreateAsync(request);
    await _cache.SetAsync($"product-service:product:{product.Id}", product, TimeSpan.FromHours(24));
    return product;
}

Perbandingan Strategi

Aspek Cache-Aside Write-Through Write-Behind
Konsistensi Eventual Strong Eventual
Write Perf Normal Sedikit lambat Sangat tinggi
Risiko data loss Rendah Rendah Ada
Use Case Read-heavy Read-after-write High write throughput

Panduan TTL per Jenis Data

Jenis Data TTL yang Disarankan Alasan
Detail produk 24 jam Jarang berubah, read-heavy
Profil pengguna 24 jam Jarang diupdate
Harga produk 1-6 jam Bisa berubah karena promo
Stok produk 5-15 menit Sering berubah, akurasi penting
Keranjang belanja 30 menit - 1 jam Data sesi, perlu fresh
Token autentikasi Sesuai expiry token Keamanan kritis

Tambahkan jitter pada TTL untuk menghindari thundering herd:

// Tambahkan variasi ±10% agar cache tidak expire bersamaan
private TimeSpan GetTtlWithJitter(TimeSpan baseTtl)
{
    var jitterFactor = 0.9 + (Random.Shared.NextDouble() * 0.2); // 0.9 - 1.1
    return TimeSpan.FromMilliseconds(baseTtl.TotalMilliseconds * jitterFactor);
}

Cache Key Helper

// Centralize key generation untuk konsistensi {service}:{entity}:{id}
public static class CacheKeys
{
    public static string Product(int id) => $"product-service:product:{id}";
    public static string Order(int id) => $"order-service:order:{id}";
    public static string UserCart(int userId) => $"cart-service:cart:user:{userId}";
    public static string UserProfile(int userId) => $"user-service:user:{userId}";
}

// ❌ BURUK — key tidak konsisten
await _cache.SetAsync("p123", product, ttl);
await _cache.SetAsync($"product:{id}", product, ttl);

// ✅ BAIK — pakai helper
await _cache.SetAsync(CacheKeys.Product(id), product, ttl);

L1 + L2 Multi-Tier Cache

Request → L1 Cache (in-process, <1ms)
              ↓ miss
          L2 Cache (Redis, ~1-2ms)
              ↓ miss
          Database (~10-100ms)
// Multi-tier: L1 (IMemoryCache) + L2 (Redis)
public async Task<Product?> GetProductAsync(int productId)
{
    var key = CacheKeys.Product(productId);

    // Coba L1 (in-process) dulu
    if (_l1Cache.TryGetValue(key, out Product? product))
        return product;

    // Coba L2 (Redis)
    var cached = await _l2Cache.GetAsync<Product>(key);
    if (cached != null)
    {
        _l1Cache.Set(key, cached, TimeSpan.FromMinutes(5)); // Populate L1
        return cached;
    }

    // Kedua miss — ambil dari DB
    product = await _repository.GetByIdAsync(productId);
    if (product != null)
    {
        await _l2Cache.SetAsync(key, product, TimeSpan.FromHours(24));
        _l1Cache.Set(key, product, TimeSpan.FromMinutes(5));
    }
    return product;
}
Tier Teknologi Latency Scope TTL Typical
L1 IMemoryCache < 1ms Per instance 1-10 menit
L2 Redis 1-5ms Semua instance 1-24 jam
Source Database 10-100ms Persistent N/A

Cache Stampede Mitigation

Solusi 1: Mutex Lock — hanya satu request yang regenerasi cache:

public async Task<T?> GetOrSetAsync<T>(string key, Func<Task<T>> factory, TimeSpan ttl)
{
    var cached = await _cache.GetAsync<T>(key);
    if (cached != null) return cached;

    var semaphore = _locks.GetOrAdd(key, _ => new SemaphoreSlim(1, 1));
    await semaphore.WaitAsync();
    try
    {
        // Double-check setelah acquire lock
        cached = await _cache.GetAsync<T>(key);
        if (cached != null) return cached;

        var freshData = await factory();
        await _cache.SetAsync(key, freshData, ttl);
        return freshData;
    }
    finally { semaphore.Release(); }
}

Solusi 2: Cache Warming — isi cache sebelum dibutuhkan:

// Background service preload data populer saat startup
public class CacheWarmupService : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken ct)
    {
        var popularProducts = await _repo.GetTopSellingAsync(100);
        foreach (var product in popularProducts)
            await _cache.SetAsync(CacheKeys.Product(product.Id), product, TimeSpan.FromHours(24));
    }
}

Aturan / Rules

Mandatory

Mandatory Data yang diambil by primary key harus di-cache. Invalidate saat update/delete.

Mandatory Tentukan strategi caching per use case: Cache-Aside, Write-Through, atau Write-Behind.

Mandatory Setiap entry cache punya TTL eksplisit. TTL minimum 24 jam untuk cache database-backed, kecuali ada aturan bisnis lain.

Mandatory Cache key mengikuti konvensi konsisten: {service}:{entity}:{id}.

Optional

Optional Bedakan tier cache: L1 (in-process) & L2 (Redis, shared).

Optional Lindungi dari cache stampede: probabilistic early expiration atau mutex lock saat miss.

Optional Jangan cache data sensitif (PII, kredensial) tanpa enkripsi at-rest.


Service Bus & Event-Driven

Apa ini? / What is this?

ID: Service Bus adalah jalur komunikasi (message broker) yang dipakai service untuk saling kirim pesan tanpa langsung memanggil satu sama lain. Event-driven berarti service bereaksi pada kejadian (event) — bukan menunggu di-perintah.

EN: A service bus is a communication channel (message broker) services use to exchange messages without calling each other directly. Event-driven means services react to events — rather than waiting to be told.

Mengapa penting? / Why it matters?

  • Loose coupling — service A tidak perlu tahu siapa yang konsumsi event-nya.
  • Tahan banting — jika consumer down, pesan menunggu di broker, tidak hilang.
  • Mudah menambah consumer baru — tambahkan subscriber tanpa mengubah producer.

Use Case

Skenario: Saat Order dibuat di Order Service, ada banyak yang perlu bereaksi: Inventory kurangi stok, Email kirim konfirmasi, Loyalty hitung poin, Analytics catat metrik.

Tanpa event: Order Service harus memanggil 4 service satu per satu (HTTP). Jika salah satu lambat/down → Order ikut bermasalah. Penambahan service ke-5 = ubah Order Service.

Dengan event: Order Service publish OrderPlaced ke broker → 4 service (& service ke-5 nanti) berlangganan event itu independen.

Istilah & Konsep / Glossary

  • Event — fakta yang sudah terjadi (mis. OrderPlaced, PaymentReceived). Past tense.
  • Command — perintah untuk melakukan sesuatu (mis. PlaceOrder). Imperative.
  • Producer / Publisher — service yang mengirim pesan.
  • Consumer / Subscriber — service yang menerima & memproses pesan.
  • Topic / Queue — saluran tempat pesan ditampung. Queue: satu pesan dikonsumsi satu consumer. Topic (pub/sub): satu pesan bisa diterima banyak subscriber.
  • Idempotent Consumer — consumer aman menerima pesan yang sama berkali-kali tanpa efek berbeda (penting karena broker bisa redeliver).
  • Outbox Pattern — pola untuk menjamin event terkirim setelah DB transaction commit. Event ditulis ke tabel outbox di transaksi yang sama dengan data bisnis; proses relay membaca tabel ini dan publish ke broker.
  • Message Broker — middleware yang menampung & meneruskan pesan (RabbitMQ, Kafka, NATS, Azure Service Bus).

Anti-pattern

  • ❌ Publish event di awal transaksi, baru DB commit di akhir → event terkirim, DB rollback = data tidak konsisten.
  • ❌ Consumer tidak dedup pesan → pesan dobel = data dobel.
  • ❌ Event tanpa schema versioning → ubah field memecah semua consumer.

Aturan / Rules

Mandatory — Event-Driven

Mandatory Setiap event membawa: event ID, event type, aggregate ID, payload, timestamp.

Mandatory Event harus idempotent di sisi consumer — consumer dedup berdasarkan event ID.

Optional — Outbox Pattern

Optional Semua domain event ditulis ke tabel outbox dalam transaksi yang sama dengan operasi bisnis.

Optional Proses relay khusus membaca outbox dan publish ke broker — memisahkan write dari publish.


Idempotency

Apa ini? / What is this?

ID: Operasi idempotent adalah operasi yang aman dijalankan berkali-kali — hasilnya sama seperti dijalankan sekali. Bayangkan tombol lift: ditekan 5x atau 1x sama saja, lift tetap dipanggil sekali.

EN: An idempotent operation is safe to run multiple times — the result is the same as running it once. Like a lift button: pressing 5 times or 1 time, the lift is still called once.

Mengapa penting? / Why it matters?

Di jaringan, request bisa: gagal di tengah jalan, timeout, atau client tidak yakin sudah sampai → client retry. Tanpa idempotency, retry = efek dobel (mis. tagihan dobel, email terkirim dua kali).

Use Case

Skenario: User klik "Bayar Rp 500.000". Browser kirim request, server mulai proses, tapi koneksi putus sebelum response sampai ke browser. User panik, klik "Bayar" lagi.

Tanpa idempotency: dua payment ter-charge → user marah, refund manual.

Dengan idempotency: client kirim header Idempotency-Key: abc-123 di kedua request. Server kedua kalinya mendeteksi key yang sama → tidak charge ulang, kembalikan response yang sama.

Istilah & Konsep / Glossary

  • Idempotency Key — pengenal unik yang diberikan client untuk setiap operasi. Biasanya UUID per intent user.
  • At-least-once delivery — broker bisa mengirim pesan lebih dari satu kali → consumer wajib idempotent.
  • Exactly-once — sangat sulit dicapai di sistem terdistribusi. Idempotent consumer + at-least-once = efek seperti exactly-once.

Anti-pattern

  • ❌ Mengandalkan client untuk tidak men-retry → pasti gagal cepat atau lambat.
  • ❌ Cek key di memory aplikasi → restart proses = key hilang.
  • ❌ Simpan key tanpa atomic dengan operasi bisnisnya → race condition.

Aturan / Rules

Optional Semua POST dan PUT yang mungkin di-retry harus idempotent.

Optional Client mengirim header Idempotency-Key untuk operasi write.

Optional Server menyimpan & menghormati idempotency key minimal 24 jam.

Optional Request duplikat dengan key sama → kembalikan response yang sama dengan request asli, tanpa mengeksekusi side-effect ulang.

Optional Penyimpanan idempotency key bersifat atomic dengan operasi yang dijaganya.


Ringkasan / Summary

  • Microservice memberi keleluasaan scale & deploy mandiri, dengan biaya kompleksitas terdistribusi.
  • Async & concurrency memungkinkan satu server melayani banyak hal sekaligus, tapi butuh disiplin sinkronisasi.
  • Data access yang baik = abstraksi (Repository) + pagination + hindari N+1.
  • Cache memindahkan beban dari DB ke memory; kuncinya TTL & invalidasi.
  • Event-driven memberi loose coupling; pakai Outbox Pattern untuk reliability.
  • Idempotency melindungi sistem dari retry → operasi penting jangan double-charge.

Selanjutnya: 3. Security — bagaimana melindungi aplikasi dari ancaman.