Архитектура бэкенда
Идемпотентность и retry: собери effectively-once платёжный путь
Читать про effectively-once — не то же, что доказать, что твой сервис никогда не списывает дважды. Собери небольшой платёжный путь от и до — идемпотентный эндпоинт, retry-клиент, outbox-relay, идемпотентный консьюмер — затем атакуй его дубликатами запросов, упавшим relay и сетью, съедающей ответы, и покажи, что side effect случается ровно один раз каждый раз.
Преврати ментальную модель юнита в работающую систему: серверную машину состояний idempotency, клиента, который делает retry с full jitter, outbox + inbox, закрывающий dual-write разрыв, и наблюдаемость, которая позволяет утверждать effectively-once доказательством, а не верой.
Собери сервис POST /charge плюс нижестоящий консьюмер инвентаря/ledger, которые вместе дают effectively-once поведение, затем докажи — под инъекцией дубликатов запросов, конкурентности, потерянного ответа и сбоя relay — что денежный side effect срабатывает ровно один раз на логическую операцию.
- Под 1000 логических операций списания с инъекцией дубликатов и конкурентности число применённых side effect равно ровно 1000 — без двойного списания, без потери — показано из метрик, а не утверждено.
- Тест конкурентности отправляет два одновременных запроса с одним ключом и показывает, что ровно один обрабатывается (200), а другой получает 409 или replay, никогда не два списания.
- Тест сбоя relay (убить после публикации, до отметки published) показывает переопубликованное событие на рестарте, а dedup в inbox консьюмера держит бизнес-эффект на одном применении.
- Тест переиспользования ключа с изменённым телом возвращает 422, а transcript показывает, что retry-клиент уважил Retry-After и использовал full jitter (задержки разнесены, а не синхронизированы).
- Добавь двухуровневый кэш: горячий путь Redis SETNX, подкреплённый Postgres-ledger, и покажи, что симулированная потеря ключа Redis проваливается в Postgres и всё равно делает dedup.
- Добавь флотовый retry budget (token bucket на call site) плюс dead-letter queue для poison-сообщений после N попыток и покажи, что outage downstream не усиливается в retry-шторм.
- Добавь одностраничный on-call runbook: triage по четырём метрикам, что значат спайк 422 vs всплеск attempt-3 vs растущий outbox lag, и фикс для каждого.
- Ограничь скорость catch-up публикации relay и покажи, что большой backlog outbox (например, 18k строк после outage брокера) сливается без самим себе устроенного thundering herd на консьюмере.
Это система за каждым платёжным API, который не списывает дважды: идемпотентный эндпоинт, разрешающий каждый ключ в new/409/replay/422 с атомарным созданием, клиент с retry на full jitter под budget, outbox, делающий dual-write атомарной, inbox, делающий dedup на консьюмере, и метрики, позволяющие утверждать, что число side effect равно числу операций. Собрать это раз — и сломать намеренно дубликатами, конкурентностью, потерянными ответами и упавшим relay — это и есть то, что превращает effectively-once из фразы в то, что ты можешь отстоять в постмортеме.