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
/ordersmengembalikan 100 order, masing-masing dengan listitems. 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.
- Offset-based (
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 / Miss — hit = 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¶
// 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
Orderdibuat 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
OrderPlacedke 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
outboxdi 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-123di 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.