Idempotencja API
Bezpieczne retry bez podwójnych płatności — Idempotency Keys w Redis, Inbox Pattern dla Kafka consumers, ochrona transakcji finansowych.
6 wzorców idempotencji
Każda warstwa architektury wymaga innego podejścia — od HTTP po bazę danych i message brokera.
| Wzorzec | Warstwa | Gwarancja | Storage | TTL | Kto używa |
|---|---|---|---|---|---|
| Idempotency Key (header) | API / HTTP | Exactly-once per key | Redis / PostgreSQL | 24-48h | Stripe, PayPal, AWS |
| Natural HTTP idempotency | API / HTTP | Per HTTP method spec | Brak | N/A | REST API z PUT/DELETE |
| Database UNIQUE constraint | Database | Exactly-once insert | Baza danych | Dożywotni | Systemy płatności |
| Inbox Pattern | Message Consumer | Exactly-once consume | DB (processed_messages) | Konfigurowalne | Kafka consumers |
| Optimistic Locking | Database | Exactly-once update | DB (version column) | N/A | Concurrent updates |
| Kafka EOS (exactly-once) | Message Broker | Exactly-once end-to-end | Kafka internal | N/A | Kafka Streams pipelines |
Często zadawane pytania
Co to jest idempotencja w API i dlaczego jest kluczowa?
Idempotencja: operacja jest idempotentna gdy wielokrotne wykonanie daje ten sam wynik co jednokrotne. f(f(x)) = f(x). HTTP idempotencja: GET — idempotentny (nie modyfikuje stanu). PUT — idempotentny (zastąpienie zasobu). DELETE — idempotentny (usunięcie zasobu, 404 przy kolejnym = ten sam stan). POST — niekoniecznie idempotentny (tworzenie zasobu). PATCH — niekoniecznie idempotentny (zależy od implementacji). Problem bez idempotencji: sieć zrywa się po przetworzeniu żądania ale przed odesłaniem odpowiedzi. Klient nie wie czy operacja się udała. Retry -> duplikat (podwójna płatność, podwójne zamówienie). Rozwiązanie: Idempotency Keys. Klient generuje unikalny klucz per żądanie. Wysyła w nagłówku: Idempotency-Key: UUID. Serwer: sprawdź czy klucz istnieje w cache/DB. Jeśli tak — zwróć poprzednią odpowiedź (bez ponownego przetwarzania). Jeśli nie — przetwórz i zapisz odpowiedź z kluczem. Stripe używa Idempotency-Key. PayPal PayPal-Request-Id. AWS API X-Amzn-Idempotency-Token. Idempotency Key storage: Redis (TTL 24h). PostgreSQL (idempotency_keys tabela). DynamoDB (conditional writes).
Jak zaimplementować Idempotency Keys na serwerze?
Implementacja serwera — kluczowe kroki: 1. Odczytaj Idempotency-Key z nagłówka. Jeśli brak — zwróć 400 Bad Request (dla operacji nie-idempotentnych). 2. Sprawdź w Redis/DB czy klucz istnieje. 3. Jeśli istnieje i status = COMPLETED — zwróć cached response. 4. Jeśli istnieje i status = IN_PROGRESS — zwróć 409 Conflict (duplicate concurrent request). 5. Zapisz klucz ze statusem IN_PROGRESS (atomic check-and-set). 6. Wykonaj operację (transakcja biznesowa). 7. Zapisz response z statusem COMPLETED. 8. Zwróć response. Race condition: dwa równoległe requesty z tym samym kluczem. Rozwiązanie: Redis SET key NX EX 86400 — atomic lock. Tylko jeden request wygrywa, drugi dostaje 409. PostgreSQL: INSERT INTO idempotency_keys (key, status) VALUES ($1, 'IN_PROGRESS') ON CONFLICT DO NOTHING — sprawdź affected rows. Redis Lua script dla atomowego check-and-set. Idempotent zapis w DB: przy retry mogą pojawić się duplikaty w DB. Rozwiązanie: unikalne constraint na kolumnie (np. external_reference_id). INSERT ... ON CONFLICT DO NOTHING — bezpieczny upsert. Zakres klucza: per user (klucz ważny tylko dla danego użytkownika). per endpoint (ten sam klucz w innym endpoint = inny zasób). TTL: 24-48h standardowo. Stripe: 24h. AWS: per-service, często 7 dni.
Idempotent consumers w message brokerach — jak zapewnić exactly-once?
Problem: message broker gwarantuje at-least-once delivery. Consumer może dostać tę samą wiadomość wielokrotnie (retry po ack timeout, rebalance). Aplikacja musi obsłużyć duplikaty. Strategie idempotent consumer: Deduplikacja przez ID: każda wiadomość ma unikalny message_id. Consumer sprawdza w DB/Redis czy message_id już przetworzony. Jeśli tak — skip (ack wiadomość bez przetwarzania). Idempotent operacja: operacja sama z siebie jest idempotentna. UPDATE SET balance = 100 WHERE id = 1 — zawsze ten sam wynik. Problem: UPDATE SET balance = balance - 10 — nie jest idempotentna. Natural idempotency: użyj SET/REPLACE zamiast ADD. Versioned updates: optimistic locking — WHERE version = X AND SET version = X+1. Reject jeśli version nie pasuje. Inbox Pattern (Transactional Outbox): w DB tabela 'processed_messages' z message_id. INSERT ... ON CONFLICT DO NOTHING przed przetworzeniem. Atomowe z przetwarzaniem (ta sama transakcja). Kafka Consumer Groups: Kafka offset commit po przetworzeniu. Enable.auto.commit = false. Manual commit po sukcesie. Problem: crash po przetworzeniu ale przed commit -> replay. Rozwiązanie: Inbox Pattern lub idempotent operations. Kafka Streams: exactly-once.v2 (EOS) — kombinacja Kafka transactions i idempotent producers. Najlepsza gwarancja ale kosztem latencji i throughpucie.
Idempotencja przy płatnościach — jak chronić przed podwójną opłatą?
Podwójna płatność: najkosztowniejszy bug w e-commerce. Klient kliknie 'Pay' dwa razy. Timeout i retry przez klienta. Network error po przetworzeniu. Warstwy ochrony: Frontend: disable przycisk po kliknięciu. Toast/spinner podczas przetwarzania. Nie pokazuj formularza płatności dwa razy. Backend Idempotency Key: klient generuje UUID per próbę płatności. Wysyła w nagłówku Idempotency-Key. Backend sprawdza w Redis/DB. Duplicate -> zwróć poprzednią odpowiedź (202/201 z tym samym payment_id). Database Unique Constraint: external_reference_id (klucz z klienta lub order_id + timestamp) jako UNIQUE. INSERT ON CONFLICT DO NOTHING. Payment Gateway idempotency: Stripe Idempotency-Key header. PayPal PayPal-Request-Id. Braintree własny mechanizm. Optimistic Locking: Order status: PENDING -> PROCESSING -> COMPLETED. UPDATE orders SET status = 'PROCESSING' WHERE id = X AND status = 'PENDING'. Jeśli 0 rows affected -> już przetwarzane. Auditlog: zapisuj każdą próbę z wynikiem. Łatwa diagnoza incydentów. Monitorowanie: metryki - duplicate_request_rate. Alert gdy wzrasta. Testy: scenariusze retry i concurrent requests w testach integracyjnych.
PUT vs PATCH — które HTTP metody są idempotentne?
PUT — zawsze idempotentny: zastąp cały zasób. PUT /users/123 {name: 'Jan', age: 30} — wynik zawsze ten sam, niezależnie ile razy wykonasz. Pełny zasób musi być w body. PATCH — może być nieide mpotentny: częściowa aktualizacja. PATCH /account {action: 'increment', amount: 10} — wielokrotne wykonanie sumuje. Idempotentny PATCH: PATCH /account {balance: 100} — ustawianie wartości, nie dodawanie. Wymaga przemyślanego API design. DELETE — idempotentny (zazwyczaj): pierwsza odpowiedź 200/204. Kolejne: 404 lub 204 (zależy od implementacji). Ważne: stan jest taki sam — zasób usunięty. POST — zwykle nie-idempotentny: POST /orders — tworzy nowe zamówienie per request. POST /login — może być idempotentny (ten sam token). POST /payments — nie-idempotentny bez Idempotency-Key. Safe vs Idempotent: Safe = nie modyfikuje stanu (GET, HEAD, OPTIONS). Idempotent = wielokrotne = jednokrotne (GET, PUT, DELETE, HEAD, OPTIONS). POST i PATCH mogą być nie-idempotentne. Projektowanie API: preferuj PUT dla aktualizacji gdy chcesz idempotencji. Używaj Idempotency-Key dla POST operacji które muszą być bezpieczne przy retry. Unikaj PATCH z increment/decrement semantics. REST Maturity Model (Richardson): Level 2+ wymaga prawidłowego użycia HTTP metod.
Powiązane artykuły
Skontaktuj się z nami
Porozmawiajmy o Twoim projekcie. Bezpłatna wycena w ciągu 24 godzin.
Wyślij zapytanie
Telefon
+48 790 814 814
Pon-Pt: 9:00 - 18:00
adam@fotz.pl
Odpowiadamy w ciągu 24h
Adres
Plac Wolności 16
61-739 Poznań
Godziny pracy
Wolisz porozmawiać?
Zadzwoń teraz i porozmawiaj z naszym specjalistą o Twoim projekcie.
Zadzwoń teraz