Czym jest idempotencja? Dlaczego interfejsy API dla księgowości i płatności jej potrzebują
W jaki sposób ponowione żądanie tworzy duplikat faktury, co gwarantuje klucz idempotencji, z jakimi trybami awarii musi sobie radzić oraz jak poprawnie go wdrożyć.
Operacja jest idempotentywna, jeśli jej dwukrotne wykonanie daje taki sam rezultat jak jednokrotne. Odczyt rekordu jest z natury idempotentywny. Tworzenie faktury już nie - wywołaj tę operację dwa razy, a otrzymasz dwie faktury, dwie należności oraz klienta pytającego, dlaczego został obciążony dwukrotnie.
Ta różnica ma fundamentalne znaczenie, ponieważ sieci komputerowe ulegają awariom w specyficzny, uciążliwy sposób: żądanie kończy się sukcesem, ale odpowiedź zostaje utracona. Klient widzi przekroczenie czasu oczekiwania (timeout) i nie jest w stanie odróżnić sytuacji, w której żądanie „nigdy nie dotarło”, od tej, w której „dotarło, zadziałało, ale odpowiedź zaginęła”. Jedyną bezpieczną opcją wydaje się ponowienie żądania, a to właśnie ponowienie tworzy duplikat.
Dlaczego w księgowości jest to jeszcze większy problem
W większości systemów duplikat jest jedynie niedogodnością. W księdze głównej staje się on problemem narastającym:
- Duplikat faktury zużywa numer z ciągu bezlukowego (gapless sequence), więc jego usunięcie nie wchodzi w grę - niezmienność danych oznacza, że korekta wymaga wystawienia faktury korygującej, czyli dokumentu, który widzi Twój klient.
- Duplikat generuje wpisy w dzienniku, przez co przychody, podatek VAT oraz należności są zawyżone do momentu, aż ktoś to zauważy.
- Trafia on do deklaracji VAT oraz do rejestrów i.SAF na Litwie.
- Jeśli faktura wywołała płatność, doszło do faktycznego przepływu środków.
Metoda „po prostu sprawdź duplikaty” również nie działa, ponieważ dwie całkowicie niezależne faktury dla tego samego klienta, na tę samą kwotę i wystawione tego samego dnia są zjawiskiem całkowicie normalnym. System nie jest w stanie odczytać intencji na podstawie samej treści - to klient musi ją zadeklarować.
Jak działają klucze idempotencji
Klient generuje unikalny klucz dla każdej operacji logicznej i przesyła go wraz z żądaniem:
POST /v1/sales/invoices/create
Idempotency-Key: 8f14e45f-ea1b-4c2b-9f3a-2d7c1e0b5a91
Serwer gwarantuje wówczas, że:
- Pierwsze żądanie z tym kluczem - przetwarza je normalnie i zapisuje wynik przypisany do tego klucza.
- Ponowienie żądania z tym samym kluczem i tą samą treścią (payload) - nie wykonuje operacji ponownie, lecz zwraca zapisaną odpowiedź.
- Ten sam klucz, ale inna treść - odrzuca żądanie. Klucz jest deklaracją, że chodzi o tę samą operację; inna treść oznacza, że tak nie jest, a ciche zwrócenie starego wyniku byłoby gorsze niż błąd.
- Ponowienie żądania w czasie, gdy pierwsze jest nadal przetwarzane - odrzuca je z błędem kwalifikującym się do ponowienia zamiast wykonywać operację współbieżnie.
Czwarty punkt jest tym, który naiwne implementacje często pomijają. Klient, u którego po 5 sekundach upłynął limit czasu i który ponawia próbę po 6 sekundach, zderzy się z własnym żądaniem będącym w toku. Bez zabezpieczenia przed współbieżnością oba żądania zostaną przetworzone i powstaną duplikaty, przed którymi mechanizm miał chronić.
Poprawne generowanie kluczy
Klucz musi identyfikować operację, a nie próbę jej wykonania:
- Poprawnie: uniwersalny identyfikator (UUID) wygenerowany raz, gdy użytkownik kliknie „wystaw fakturę”, wykorzystywany ponownie przy każdym ponowieniu tego kliknięcia.
- Poprawnie: klucz deterministyczny wywodzący się z własnej domeny biznesowej -
order-1041-invoice. - Niepoprawnie: nowy UUID przy każdej próbie HTTP. Każde ponowienie otrzymuje nowy klucz, więc każde ponowienie jest traktowane jako nowa operacja, co oznacza, że mechanizm w ogóle nie został wdrożony.
- Niepoprawnie: skrót (hash) treści żądania. Dwie w pełni poprawne, identyczne operacje wywołają wtedy kolizję, a druga zostanie po cichu pominięta.
Klucze wymagają okresu przechowywania - na tyle długiego, aby objąć realistyczne okna ponowień i awarie po stronie klienta, ale nie wiecznie.
Czego idempotencja nie rozwiązuje
Idempotencja chroni przed dwukrotnym wykonaniem tego samego żądania. Nie chroni przed:
- Dwóch różnych użytkowników tworzących tę samą fakturę z różnych sesji - to problem reguł biznesowych, rozwiązywany za pomocą ograniczeń unikalności, takich jak zasada jednej faktury na zamówienie.
- Duplikatami logicznymi z różnymi kluczami.
- Niemodeterminizmem wewnątrz operacji - jeśli procedura obsługi generuje znacznik czasu lub numer, zapisana odpowiedź przechwytuje pierwszą wartość, co jest pożądanym zachowaniem, ale warto być tego świadomym.
Mechanizm ten uzupełnia ograniczenia bazodanowe, zamiast je zastępować. Rejestr (ledger) Nordlet stosuje oba rozwiązania: klucze idempotencji powstrzymują ponawiane żądania, podczas gdy unikalne indeksy zapobiegają duplikatom partii rozliczeniowych w przeliczeniu na identyfikator wypłaty, duplikatom faktur zakupowych w przeliczeniu na dostawcę i numer dokumentu oraz duplikatom przebiegów amortyzacji w przeliczeniu na środek trwały i okres.
Jak idempotencję wdrożył Nordlet
Prześlij nagłówek Idempotency-Key przy każdym żądaniu zmieniającym stan - ma to być niepusty ciąg znaków o długości maksymalnie 255 znaków.
Pierwsze żądanie z danym kluczem zostaje wykonane, a jego wynik zapisany. Ponowienie żądania z tym samym kluczem i tą samą treścią zwraca zapisaną odpowiedź bez ponownego wykonywania operacji, dzięki czemu faktura tworzy się dokładnie raz, niezależnie od liczby powtórzeń wywołania.
Dwa stany awaryjne są obsługiwane w sposób jawny, a nie cichy:
- Ten sam klucz, inna treść → odrzucenie: "Idempotency-Key was already used with a different request payload." Pozwala to wykryć naprawdę niebezpieczną sytuację, w której klient używa ponowne tego samego klucza, zmieniając jednocześnie kwotę.
- Ten sam klucz, pierwsze żądanie nadal w toku → odrzucenie z informacją o możliwości ponowienia: "A request with this Idempotency-Key is still being processed, retry shortly." Zapobiega to współbieżnemu, podwójnemu wykonaniu.
Klucze są powiązane z konkretną firmą (zakres w ramach tenant), więc dwaj różni klienci generujący ten sam UUID nigdy nie wywołają kolizji.
Uzupełniająca gwarancja działa w przeciwnym kierunku. Dostarczanie webhooków odbywa się co najmniej raz (at-least-once), co oznacza, że Twój punkt końcowy (endpoint) od czasu do czasu otrzyma to samo zdarzenie dwukrotnie - po przekroczeniu czasu oczekiwania, kiedy dostarczenie w rzeczywistości się powiodło, lub przy ponownym dostarczeniu. Każde zdarzenie posiada identyfikator, a Twoja procedura obsługi powinna rejestrować przetworzone identyfikator i ignorować powtórzenia. Idempotencja to cecha, której potrzebują obie strony integracji; przewodnik po konwencjach API szczegółowo omawia stronę żądań.
FAQ
Czym jest klucz idempotencji?
To generowany przez klienta identyfikator przesyłany wraz z żądaniem, który pozwala serwerowi rozpoznać ponowienie tej samej operacji logicznej. Pierwsze żądanie jest wykonywane; ponowienia z tym samym kluczem zwracają zapisany wynik zamiast ponownego wykonywania operacji.
Dlaczego interfejsy API dla księgowości potrzebują idempotencji?
Ponieważ zduplikowany zapis tworzy rzeczywisty dokument o realnych konsekwencjach - drugą fakturę pobierającą numer sekwencyjny, trafiającą do księgi głównej oraz do deklaracji VAT. W przeciwieństwie do zduplikowanego odczytu, nie można jej po prostu usunąć; trzeba ją skorygować za pomocą faktury korygującej.
Czy powinienem generować nowy klucz dla każdego ponowienia?
Nie - to całkowicie niweczy sens tego mechanizmu. Generuj jeden klucz dla każdej operacji logicznej i używaj go ponownie przy każdym ponowieniu tej operacji. Nowy klucz dla każdej próby oznacza, że każda próba jest traktowana jako nowa operacja.
Co się stanie, jeśli użyję ponowne klucza z inną treścią żądania?
Żądanie zostanie odrzucone. Klucz potwierdza, że jest to ta sama operacja, więc zmieniona treść oznacza, że coś jest nie w porządku - najprawdopodobniej błąd ponownego użycia klucza po stronie klienta. Ciche zwrócenie wcześniejszego wyniku byłoby gorszym rozwiązaniem.
Czy idempotencja zapobiega wszystkim duplikatom?
Nie. Zapobiega ona zduplikowanemu wykonaniu tego samego żądania. Dwóch różnych użytkowników niezależnie tworzących tę samą fakturę generuje dwa różne żądania, co stanowi problem reguł biznesowych dla ograniczeń unikalności, a nie kwestię, którą klucz idempotencji jest w stanie wykryć.