Idempotency
Proteksi duplikasi request menggunakan header Idempotency-Key
Idempotency melindungi endpoint mutasi kritis dari duplikasi data akibat retry. Ketika response gagal diterima client karena network timeout, client melakukan retry yang menyebabkan operasi dieksekusi ulang. Fitur ini menyimpan response sukses di Redis sehingga retry dengan key yang sama mengembalikan response tersimpan tanpa mengeksekusi operasi ulang.
Referensi Cepat (Quick Reference)
| Properti | Nilai |
|---|---|
| Dependency | Redis Server 6.0+, CACHE_ENABLED=true |
| Aktivasi | IDEMPOTENCY_ENABLED=true di file .env |
| Default TTL | 300 detik (5 menit) |
| Header wajib | Idempotency-Key pada endpoint kritis |
| Endpoint dilindungi | /create, /create-composite, /adjust, /update-composite |
| Response header (replay) | Idempotent-Replayed: true |
Masalah yang Diselesaikan (Problem Statement)
Tanpa Idempotency:
1. Client kirim POST /create { supplier_code: "SUP-001" }
2. Server INSERT berhasil, UUID "aaa-111" tersimpan
3. Response GAGAL sampai ke client (network timeout)
4. Client retry → Server INSERT lagi → UUID "bbb-222" (duplikat)
Dengan Idempotency:
1. Client kirim POST /create + Header Idempotency-Key: "req-001"
2. Server INSERT berhasil, response disimpan ke Redis
3. Response GAGAL sampai ke client
4. Client retry dengan key sama → Redis HIT → return response tersimpan
5. Client menerima UUID "aaa-111" (sama, tanpa duplikasi)Endpoint yang Dilindungi (Protected Endpoints)
| Endpoint | Masalah tanpa Idempotency | Tingkat Kritis |
|---|---|---|
/create | Record duplikat (UUID baru per retry) | Tinggi |
/create-composite | Header + seluruh detail items terduplikat | Tinggi |
/adjust | Penyesuaian terakumulasi (stock + N dijalankan 2x) | Kritis |
/update-composite | Detail insert menghasilkan item duplikat | Tinggi |
Endpoint yang tidak memerlukan idempotency: /read, /first, /datatables, /lookup (read-only), /update (idempotent secara natural karena SET field = value), dan /delete (retry return 404).
Konfigurasi (Configuration)
IDEMPOTENCY_ENABLED=true
IDEMPOTENCY_TTL=300
CACHE_ENABLED=true
REDIS_HOST=redis.production.internal
REDIS_PORT=6380| Variable | Default | Keterangan |
|---|---|---|
IDEMPOTENCY_ENABLED | false | Aktifkan/nonaktifkan idempotency secara global |
IDEMPOTENCY_TTL | 300 | Durasi penyimpanan response di Redis (detik) |
CACHE_ENABLED=true wajib aktif karena idempotency menggunakan Redis via cache layer. Server menolak start jika IDEMPOTENCY_ENABLED=true tanpa CACHE_ENABLED=true.
Cara Penggunaan (How to Use)
Sertakan header Idempotency-Key pada setiap request ke endpoint kritis:
curl -X POST http://localhost:3080/api/mini-inventory/supplier/create \
-H "Content-Type: application/json" \
-H "Idempotency-Key: sup-create-001" \
-d '{
"supplier_code": "SUP-TEST",
"supplier_name": "Test Supplier"
}'Response pertama (HTTP 201):
{
"success": true,
"message": "supplier data successfully added",
"data": {
"supplier_id": "aaa-111-bbb-222",
"supplier_code": "SUP-TEST",
"supplier_name": "Test Supplier"
},
"timestamp": "2026-04-16T10:30:00.000Z"
}Response retry (dari cache, header Idempotent-Replayed: true):
Response body dan status code identik dengan request pertama. Header Idempotent-Replayed: true menandakan response ini adalah replay.
Format Idempotency-Key
| Format | Contoh | Keterangan |
|---|---|---|
| UUID | 550e8400-e29b-41d4-a716-446655440000 | Format standar, direkomendasikan |
| Custom | INV-2026-001-create | Bebas, sesuai konvensi project |
Panjang key maksimum 255 karakter.
Penanganan Error (Error Handling)
| HTTP Status | Kondisi | Penyebab |
|---|---|---|
| 400 | Missing Idempotency-Key | Request ke endpoint kritis tanpa header |
| 400 | Invalid Idempotency-Key | Key melebihi 255 karakter |
| 422 | Idempotency-Key conflict | Key sama digunakan dengan payload berbeda |
Error 422: Key Conflict
Terjadi ketika key yang sama digunakan untuk mengirim request dengan payload yang berbeda:
{
"success": false,
"error": "Idempotency-Key conflict",
"message": "This Idempotency-Key has been used with a different request body",
"timestamp": "2026-04-16T10:30:00.000Z"
}Setiap operasi bisnis yang berbeda harus menggunakan key yang unik. Key hanya boleh di-reuse untuk retry operasi yang sama (payload identik).
Pola Retry yang Benar (Correct Retry Pattern)
// Key di-generate SEBELUM loop retry
const idempotencyKey = crypto.randomUUID();
async function createWithRetry(data, maxRetries = 3) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const response = await fetch('/api/mini-inventory/supplier/create', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Idempotency-Key': idempotencyKey // key SAMA di setiap retry
},
body: JSON.stringify(data)
});
return await response.json();
} catch (error) {
if (attempt === maxRetries) throw error;
await sleep(attempt * 1000);
}
}
}Key harus di-generate sebelum loop retry, bukan di dalamnya. Key baru di setiap retry akan melewati proteksi idempotency.
Graceful Degradation
| Skenario | Perilaku |
|---|---|
| Redis tidak tersedia saat startup | Server menolak start |
| Redis terputus saat runtime | Request diteruskan tanpa proteksi idempotency |
| Redis GET error | Request dieksekusi sebagai baru |
| Redis SET error | Diabaikan, response tetap dikirim |
Penggunaan di Processor (Processor Usage)
Idempotency manager tersedia di processor melalui services.idempotency:
const { idempotency } = services;
// Generate key random untuk panggilan ke service lain
const key = idempotency.generateKey('payment');
// Hasil: "payment_550e8400-..."
// Generate key deterministik (selalu sama untuk input sama)
const key = idempotency.buildDeterministicKey('inventory', 'receive', input.inbound_id);
// Hasil: "inventory:receive:INB-2026-001"
// Cek status
if (idempotency && idempotency.isEnabled()) { ... }Langkah Selanjutnya (Next Steps)
- Cache untuk layer caching berbasis Redis
- Distributed Lock untuk koordinasi write antar worker
- Rate Limit untuk pembatasan jumlah request