Punkt wyjścia
Powracający klient (branża usługowa) prowadzi dwa bliźniacze studia na platformie no-code (encje + funkcje brzegowe Deno, 2-way sync z GitHub). Recepcja rejestruje usługi, prowadzi kasę zmianową i wypłaca pracownice. Po wcześniejszym ratunku system był stabilny — do czasu, aż urósł wolumen.
Problem
Wiadomość, od której się zaczęło: pojedyncza wypłata na 18 080 CHF, obejmująca 212 rekordów, przeszła tylko dla około połowy z nich — a system zgłosił sukces i poszedł dalej.
- Cicha awaria w połowie: oznaczonych zostało ~100 z 212 rekordów. Funkcja i tak doszła do końca i zapisała log audytu, więc wypłata wyglądała na domkniętą, a stan był niespójny.
- Zapisy z przeglądarki: cała wypłata wysyłała setki pojedynczych aktualizacji rekordów prosto z klienta. Powyżej ~200 operacji uderzała w rate limit platformy.
- Połknięte błędy:
try/catchpo cichu pochłaniał każdy nieudany zapis — bez alertu, bez rollbacku, bez statusu. Nic nie sygnalizowało, że zadziałało tylko częściowo. - Szkody uboczne: ręczny ratunek nadpisał 11 rekordów ustawień dziennych błędnym odniesieniem do wypłaty — utajone ryzyko podwójnego naliczenia.
Diagnoza
Oczywista hipoteza brzmiała „za duża współbieżność". Obaliłem ją serią kontrolowanych testów obciążeniowych:
- Współbieżność 10, bez retry: 100 / 217 oznaczonych, 117 błędów rate-limit.
- Współbieżność 5 + retry: 25 / 215 — gorzej; retry tylko zwielokrotnia liczbę operacji.
- Współbieżność 3, czyste 210: 200 / 210.
Wzorzec był jasny: wąskim gardłem była łączna liczba operacji zapisu (~250+) w jednej funkcji — nie współbieżność ani opóźnienia między nimi. Strojenie nigdy by tego nie naprawiło; trzeba było zmienić architekturę.
Rozwiązanie
1. Operacje masowe zamiast zapisów per rekord
SDK platformy ma endpointy masowe — do 500 rekordów na jedno wywołanie HTTP. Przepisałem wypłatę tak, by każdy krok wielorekordowy szedł paczkami (helper tnie na strony po 500). ~272 wywołania zwinęły się do ~10, a wypłata 210 rekordów spadła z ~126 s (padającej) do 3,9 sekundy.
2. Prawdziwa maszyna stanów na każdej wypłacie
Każda wypłata startuje jako pending i staje się committed
dopiero, gdy wszystkie kroki się powiodą — w przeciwnym razie failed, z
zapisaną listą kroków, które padły. Żadna operacja nie może skończyć „w połowie" i
udawać, że jest dobrze. Pełna idempotencja sprawia, że retry nigdy nie naliczy
podwójnie.
3. Monitoring, który się odzywa — i tylko wtedy
Zaplanowany nocny audyt skanuje ostatnie wypłaty pod kątem niespójności i pinguje właściciela na Telegramie tylko wtedy, gdy coś jest nie tak. Gdy wszystko jest czyste — milczy. Powiadomienie pracownicy wychodzi dopiero po udanym commicie.
4. Naprawa jednym kliknięciem
Każda wypłata zawieszona w pending lub failed pojawia się
w interfejsie jako czerwony baner z przyciskiem „Dokończ wypłatę" — idempotentna
procedura naprawcza, która bezpiecznie domyka brakujące zapisy. Rzadka awaria staje
się jednym kliknięciem, a nie operacją na danych.
Rezultat
Oba studia działają teraz na tym samym silniku. Wypłata 210 rekordów zajmuje ~3,9 sekundy i nie może już paść po cichu: jeśli cokolwiek się zepsuje, właściciel wie o tym zanim dowie się pracownica, i naprawia to jednym kliknięciem. Audyt ostatnich 30 dni potwierdził, że poza pierwotnym incydentem wszystkie historyczne wypłaty są czyste. Całe wdrożenie odbyło się z zerowym przestojem produkcji — stary mechanizm został jako 30-sekundowy rollback, a każda zmiana była testowana na koncie jednorazowym, nigdy na danych prawdziwych pracownic.
Wnioski
Naprawa, która naprawdę się liczyła, nie dotyczyła szybkości — tylko widoczności. System obracający pieniędzmi nigdy nie może mieć prawa paść po cichu. Gdy wypłaty ruszają realną gotówką, „doszło do końca bez wyjątku" to nie to samo co „zadziałało".
Platformy no-code skalują się znakomicie — do momentu, gdy pojedyncza operacja musi dotknąć setek rekordów naraz. To jest granica, na której kończy się „klikanie razem", a zaczyna inżynieria: batchowanie, atomowość, idempotencja, monitoring.