Паттерны бэкенда, часть 1: отказоустойчивость и консистентность
Circuit breaker, деградация, повторы, rate limiting, события, outbox, inbox, идемпотентность, DLQ и саги — на одном сервисе уведомлений, с анимациями и продакшн-кейсами
Эти паттерны спрашивают на собеседованиях на middle и senior, и отвечать определением из документации — худшее, что можно сделать. Спрашивают не «что такое circuit breaker», а «что у вас происходило, когда падал Redis»
Поэтому всё разобрано на одной системе — сервисе уведомлений. Он маленький, но в нём есть всё: входящие события, чужой API, кеш, база, очередь на отправку и воркеры по каналам. Каждая стрелка на схеме — место, где что-то отваливалось в проде
событие → профиль → история → отправка
Notification Service — на нём и разбираем
Всё начинается с чужого события: какой-то сервис сообщил, что пользователь зарегистрировался. Notification Service читает его из Kafka и решает, что с этим делать
Совет
Как читать схемы
Каждая анимация проходит сценарий по шагам: видно запрос, видно отказ и видно, что именно сделал паттерн. Шаги переключаются кнопками внизу, пауза — кнопкой в шапке. У каждого паттерна сверху есть строка «проблема — решение»: если суть ясна, дальше можно листать
1. Circuit Breaker
Зависимость не отвечает, а сервис продолжает в неё ходить. Каждый вызов занимает соединение до таймаута — через минуту пул кончился, и падает уже всё, а не только та ручка
Считаем ошибки в окне. Больше порога — размыкаем цепь: вызовы отклоняются мгновенно, а ответ собирается из запасного источника
Самое дорогое при отказе зависимости — не сам отказ, а ваше упорство. Сто запросов в секунду по 200 мс таймаута — это двадцать занятых соединений в каждый момент. Ещё немного, и сервис отвечает пятисотками на всё подряд, включая ручки, которым Redis вообще не нужен
Брейкер ставит между вами и зависимостью счётчик. Пока ошибок мало, он прозрачен. Как только их доля переваливает порог, он перестаёт пропускать вызовы вообще — и тем самым даёт упавшему сервису подняться
closed → open → half-open
Redis отвалился, лента уведомлений живёт
Брейкер закрыт. Каждый запрос ленты идёт в Redis и возвращается за 4 мс — это обычный горячий путь
- Closed — трафик идёт как обычно, брейкер только считает ошибки в скользящем окне
- Open — порог пройден, вызовы отклоняются сразу, без похода в сеть
- Half-open — по таймеру пропускается один пробный вызов: ответ есть — закрываемся, нет — окно начинается заново
Памятка
Продакшн-кейс
Лента уведомлений читается из Redis. Когда кеш недоступен, брейкер открывается и лента собирается из in-memory LRU на 5 тысяч записей. Данные отстают на минуту, зато экран открывается — вместо пустоты со спиннером
Не делай так
Две ошибки, которые убивают пользу
Брейкер без запасного пути просто меняет медленный отказ на быстрый — польза появляется только там, где за ним стоит fallback. И порог считают в долях, а не в штуках: 20 ошибок срабатывает и на тысяче запросов в секунду, и на десяти, хотя это совершенно разные ситуации
2. Graceful degradation
Экран собран из нескольких источников, и отказ любого роняет весь ответ. Аватарки не пришли — пользователь не видит и списка уведомлений
Заранее решаем, что ядро, а что украшение. Украшение при отказе подменяется заглушкой или устаревшим значением, ядро — единственное, ради чего отдаём 503
Источники у экрана разные по важности, но в коде это обычно ничем не выражено: всё лежит в одном Promise.all, и любой reject роняет всё. История уведомлений — смысл экрана. Аватарки рядом с ними — украшение. Для Promise.all это одно и то же
Деградация — это не «постараемся, чтобы не упало». Это заранее составленный список: вот ядро, вот это можно отдать заглушкой, вот это — старым значением с честной пометкой
полный ответ → усечённый ответ
Половина зависимостей лежит, экран всё ещё работает
Экран уведомлений собирается из трёх источников: история из PostgreSQL, аватарки из профилей, счётчик непрочитанного
- Разметьте зависимости на критичные и необязательные — до инцидента, а не во время
- У каждого необязательного вызова свой таймаут и своё «что вернуть, если не вышло»
- Старые данные помечаются старыми — задержку простят, молчаливое враньё нет
- Отказ ядра — это честный 503 с понятным текстом, а не белый экран
Памятка
Продакшн-кейс
Сервис профилей отдаёт 503 — вместо фотографий рисуются инициалы, список приходит целиком. Отвалился счётчик непрочитанного — показываем последнее известное значение с пометкой, что оно двухминутной давности
Совет
Проверить разметку можно одним вопросом: «если этот сервис ляжет на час, что увидит пользователь». Если ответ «белый экран» — зависимость критичная, как бы её ни называли в документации
3. Retry с экспоненциальной паузой
Провайдер прилёг на десять секунд — задача потеряна. А если повторять сразу и из всех подов, вы добьёте его синхронным залпом ровно в момент подъёма
Повторяем только временные ошибки, с растущей паузой и случайным разбросом. Пять попыток — и в DLQ, а не бесконечный круг
Повтор лечит только временные сбои: сеть моргнула, провайдер перезагрузил ноду, база переключила мастера. Против ошибки в данных он бесполезен — сколько ни отправляй письмо на несуществующий адрес, оно не дойдёт
Опаснее другое. Внешний сервис прилёг — и повторы летят в него от всех ваших подов одновременно. Он поднимается, получает залп и падает снова. Так вы своими руками устраиваете ему DDoS в момент, когда он слабее всего
retry + exponential backoff + jitter
Три воркера повторяют отправку, не добивая провайдера
Провайдер прилёг и отвечает 503 всем трём воркерам. Письма не потеряны — они просто не доставлены прямо сейчас
const BASE_MS = 1_000;
const CAP_MS = 30_000;
const JITTER = 0.2;
export function backoffDelay(attempt: number): number {
const exponential = Math.min(BASE_MS * 2 ** attempt, CAP_MS);
const spread = exponential * JITTER;
return Math.round(exponential - spread + Math.random() * spread * 2);
}
// 1 → ~2.0 c, 2 → ~4.0 c, 3 → ~8.0 c, 6 → 30 c (потолок)- Повторяем идемпотентное и только на
5xx, таймаут и обрыв соединения 400,401,422не повторяем — тело запроса не изменится, а нагрузку вы создадите- Джиттер обязателен — без него весь кластер просыпается в одну миллисекунду
- У повторов есть потолок паузы и предел попыток, дальше — DLQ
Памятка
Продакшн-кейс
Воркер писем повторяет отправку пять раз с паузами от секунды до тридцати. Что не ушло за пять попыток, уезжает в отдельный топик с причиной — там уже видно, это сломанный адрес или провайдер лежит второй час
Важно
Считайте общее время, а не число попыток
Три попытки по три секунды — это девять секунд ожидания для человека, который смотрит на спиннер. У запроса должен быть общий бюджет времени: кончился бюджет — прекращаем повторы, даже если попытки ещё остались
4. Rate limiting
Все предыдущие паттерны — про то, как пережить чужой отказ. Этот про обратное: как не дать положить себя. Один скрипт в цикле выбирает всю ёмкость, и сервис ложится для всех остальных
Счётчик на входе: сколько запросов в единицу времени можно этому клиенту. Лишнее отбивается кодом 429, не доходя ни до базы, ни до внешних провайдеров
Лимит — это не защита от злоумышленников, а защита от любой неравномерности. Забытый setInterval в чужом коде, ретрай без бэкоффа, выгрузка отчёта в цикле — все они выглядят как атака и лечатся одинаково
Классический алгоритм — token bucket: ведро с токенами, которое наполняется с постоянной скоростью. Короткий всплеск проходит за счёт накопленного, длинный поток упирается в скорость наполнения. Это честнее фиксированного окна, где на границе двух окон можно пропихнуть двойную норму
токены → 429 → ключ вместо IP
Всплеск упирается в ведро, а не в базу
В ведре 20 токенов, оно наполняется со скоростью 10 в секунду. Каждый запрос забирает один токен — так короткий всплеск проходит, а долгий поток упирается в скорость наполнения
- Считаем по владельцу —
userIdили ключ API, а не по IP - Отвечаем
429иRetry-After— клиент должен понимать, когда возвращаться - Лимит хранится в Redis, иначе у каждой реплики будет свой, и общий окажется втрое больше
- Отказ должен быть дешёвым — проверка идёт до похода в базу, иначе лимит сам станет нагрузкой
- Разные ручки — разные лимиты: выгрузка отчёта и чтение ленты не могут стоить одинаково
Не делай так
Продакшн-кейс: лимит, который считал не то
Прокси-слой между браузером и бэкендом не пробрасывал адрес клиента, и все запросы приходили с одного адреса — его собственного. Лимит «по IP» работал идеально, просто ведро было общим на всю платформу: 46 живых пользователей делили квоту одного. Симптом выглядел как «сервис тормозит у части людей», а причина была в одной недостающей строке проброса заголовка
Важно
IP — плохой идентификатор
За одним адресом сидит офис, мобильный оператор или ваш собственный прокси. За одним пользователем — десяток адресов, если он в метро. Лимит по IP годится только там, где владельца ещё нет: логин, регистрация, восстановление пароля
5. Работа с внешним API
У чужого сервиса нет ни ваших логов, ни ваших метрик, ни кнопки «перезапустить». А зависший вызов держит ваше соединение столько, сколько захочет он
Четыре слоя вокруг каждого вызова: таймаут, повтор, брейкер и отдельный пул. Каждый закрывает то, что не закрывает предыдущий
Единственное, чем вы управляете в интеграции, — как именно вы туда ходите. Три предыдущих паттерна работают здесь вместе, и порядок имеет значение: таймаут ограничивает одну попытку, повтор даёт второй шанс, брейкер прекращает попытки целиком, когда стало ясно, что шанса нет
таймаут → ретрай → брейкер → изоляция
Чужой API, который может зависнуть навсегда
HTTP-клиент по умолчанию ждёт ответа бесконечно. Провайдер не отвечает — и через минуту весь пул из 32 соединений занят висящими запросами, а сервис не принимает ничего
- Таймаут на соединение и отдельно на чтение — по умолчанию HTTP-клиенты ждут вечно
- Повтор с паузой и джиттером, только для безопасных операций
- Брейкер по каждому провайдеру отдельно, а не один на все интеграции
- Свой пул соединений на провайдера, чтобы медленный не съел воркеры остальных
- Метрики в разрезе провайдера:
p99, доля ошибок, состояние брейкера
Памятка
Продакшн-кейс
У пуш-провайдера и SMS-шлюза разные пулы и разные брейкеры. Когда пуши начинают отваливаться, сообщение уходит по SMS — а не встаёт в общую очередь и не блокирует отправку тем, у кого канал живой
Не делай так
Грабли
Один общий HTTP-клиент на все интеграции — это одна общая точка отказа. Зависший платёжный шлюз в такой схеме останавливает и отправку уведомлений, хотя между ними нет ничего общего, кроме библиотеки
6. Event-Driven Messaging
Регистрация вызывает нотификации напрямую: знает про них, ждёт их ответа и падает вместе с ними. Добавили аналитику — правим регистрацию. Добавили антифрод — правим снова
Продюсер публикует факт и забывает. Кто это читает, сколько их и живы ли они — не его дело и не его релиз
Синхронный вызов связывает два сервиса жёстче, чем кажется: он тащит за собой и знание об адресате, и его аптайм, и его скорость. Событие переворачивает зависимость — теперь потребитель знает про продюсера, а не наоборот
Событие описывает то, что уже случилось, в прошедшем времени: user.registered, а не send_welcome_email. Разница не косметическая — команда подразумевает конкретного исполнителя, факт не подразумевает никого
publish → fan-out → независимые потребители
X Service не знает, кто читает его события
X Service публикует факт: «пользователь зарегистрировался». Он не вызывает нотификации, не знает про них и не падает, если их вообще нет в кластере
- У каждого потребителя своя consumer-группа, свой оффсет и своя скорость
- Потребитель лёг — события копятся в топике, а не теряются в воздухе
- Новый потребитель подключается без единой правки у продюсера
- Порядок гарантирован только внутри партиции — ключ партиционирования выбирается осознанно
Памятка
Продакшн-кейс
Notification Service слушает user.registered и шлёт приветственное письмо. Аналитика слушает то же событие и строит воронку. Когда нотификации лежали 5 минут после релиза, письма ушли с задержкой — но ушли все, потому что события ждали в топике
Важно
Где события не подходят
Всё, где пользователь ждёт ответа прямо сейчас, остаётся синхронным. «Хватит ли денег на счёте» — вызов с ответом. «Отправить письмо о списании» — событие. Асинхронность берут за деньги: вы платите отладкой, трассировкой и невозможностью просто прочитать стектрейс
7. Transactional Outbox
Запись в базу и отправка события в брокер — две разные транзакции. Упали между ними: либо данные без события, либо событие без данных
Событие пишется строкой в ту же транзакцию, что и данные. Отдельный процесс потом вычитывает эти строки и публикует их
Код выглядит невинно: сохранили уведомление, отправили событие. Но между COMMIT и publish нет ничего, что связывало бы их вместе — это два разных хранилища с двумя разными механизмами отката
Дальше сценарий на выбор. Процесс умер после коммита — данные есть, события нет, письмо не уйдёт никогда. Событие ушло, а транзакция откатилась — письмо про платёж, которого не было. Оба случая всплывают редко и разбираются мучительно
один COMMIT → publisher → mark sent
Запись в базу и событие в Kafka не расходятся
Наивный код пишет строку в базу, а следом шлёт событие в Kafka. Между этими двумя действиями нет транзакции: упали после COMMIT — событие не ушло, упали после publish и откатили — событие есть, а данных нет
await db.transaction().execute(async trx => {
await trx.insertInto('notifications').values(notification).execute();
await trx
.insertInto('outbox')
.values({
aggregate_id: notification.id,
topic: 'notification.created',
payload: JSON.stringify(payload),
})
.execute();
});
// Kafka здесь нет. Публикует отдельный процесс — из того, что уже закоммичено- Данные и строка outbox пишутся одним
COMMIT— атомарность обеспечивает база - Publisher читает неотправленные строки с
FOR UPDATE SKIP LOCKED, поэтому его можно масштабировать - После публикации строка помечается отправленной
- Падение между publish и отметкой даёт повтор — это
at-least-onceпо определению, и потребитель обязан быть к нему готов
Памятка
Продакшн-кейс
Уведомление и запись в outbox создаются одной транзакцией. Publisher с интервалом 200 мс разбирает очередь и публикует в Kafka. За счёт SKIP LOCKED три экземпляра publisher работают параллельно и не дерутся за одни и те же строки
Совет
Поллинг можно заменить чтением WAL через Debezium — база разгружается, но в поддержке появляется ещё один компонент со своим состоянием. До нескольких тысяч событий в секунду поллинг честно выигрывает по простоте
8. Transactional Inbox
Брокер гарантирует at-least-once, то есть дубли будут. Обработали событие дважды — пользователь получил два одинаковых пуша, а клиент два списания
Идентификатор сообщения пишется в таблицу с уникальным индексом — в той же транзакции, что и эффект. Повтор отбивается конфликтом
Outbox гарантирует, что событие уйдёт хотя бы раз. Ключевое слово — «хотя бы». Ребаланс группы, повтор publisher, переподключение консьюмера: одно и то же сообщение приезжает второй раз, и это штатная работа брокера, а не сбой
Inbox — зеркальная половина outbox. Не «на всякий случай», а обязательное условие: при at-least-once дубли гарантированы, вопрос только в том, заметите вы их или пользователь
дубль → уникальный индекс → пропуск
Событие пришло дважды, пуш ушёл один раз
Приходит событие «начислен платёж». Сервис вставляет его message_id в таблицу inbox — в той же транзакции, что и сама обработка
await db.transaction().execute(async trx => {
const inserted = await trx
.insertInto('inbox_messages')
.values({ message_id: message.id, consumer: 'notification' })
.onConflict(oc => oc.columns(['message_id', 'consumer']).doNothing())
.executeTakeFirst();
// Строка уже была — событие обработано раньше, второй раз не отправляем
if (Number(inserted.numInsertedOrUpdatedRows ?? 0) === 0) return;
await sendPush(trx, message.payload);
});- Уникальный индекс по
(message_id, consumer)— дедупликация лежит на базе, а не на коде - Проверка и сам эффект живут в одной транзакции, иначе между ними снова появляется щель
- Оффсет коммитится и для дубликата — он обработан корректно, то есть осознанно пропущен
- Записи чистятся по расписанию, иначе таблица растёт вечно
Памятка
Продакшн-кейс
После ребаланса группы событие о платеже приехало дважды. Уникальный индекс отбил вставку, обработка не запустилась — пользователь получил один пуш вместо двух. В логах это видно как duplicate key, и это не ошибка, а сработавшая защита
Совет
Иногда inbox не нужен: если операция идемпотентна сама по себе — UPDATE ... SET status = paid или upsert по ключу, — повтор просто перезапишет тот же результат. Inbox нужен там, где эффект внешний и необратимый: отправка письма, списание, вызов чужого API
9. Идемпотентность и Idempotency-Key
Ответ на POST /payments не доехал до клиента. Клиент не знает, случилось списание или нет, и повторяет запрос. Наивный обработчик спишет второй раз
Клиент присылает ключ попытки. Первый запрос выполняется и сохраняет свой ответ под этим ключом, повтор получает тот же ответ — без повторного эффекта
Три предыдущих паттерна опираются на это слово: retry повторяет, outbox доставляет минимум один раз, inbox отбивает дубли на стороне потребителя. Но у публичного API нет ни inbox, ни оффсетов — там повтор приходит от живого клиента, и защищаться надо иначе
Идемпотентность — это свойство операции: повторный вызов не меняет результат. GET и DELETE идемпотентны сами по себе, PUT обычно тоже. POST — нет, и именно ему нужен ключ
ключ → сохранённый ответ → 409
Клиент повторил запрос — списание осталось одно
Клиент присылает Idempotency-Key — случайный идентификатор попытки, а не платежа. Такого ключа ещё нет, значит, операция новая
const existing = await keys.find(idempotencyKey, userId);
if (existing) {
// Тот же ключ с другим телом — это ошибка клиента, а не повтор
if (existing.requestHash !== hashOf(body)) throw new ConflictException();
return existing.response;
}
return db.transaction().execute(async trx => {
const payment = await charge(trx, body);
const response = { id: payment.id, status: payment.status };
await trx
.insertInto('idempotency_keys')
.values({ key: idempotencyKey, user_id: userId, request_hash: hashOf(body), response })
.execute();
return response;
});- Ключ генерирует клиент — это идентификатор попытки, а не платежа
- Хранится сам ответ, а не только факт «ключ был»: повтору надо что-то вернуть
- Хеш запроса ловит тот же ключ с другим телом и отдаёт
409 - Ключ скоуплен на владельца, иначе чужой запрос с угаданным ключом получит ваш ответ
- У ключа есть TTL — суток хватает на любые сетевые повторы
Памятка
Продакшн-кейс
Загрузка записи собеседования идёт с ключом идемпотентности, привязанным к пользователю. Повтор с тем же ключом возвращает исходный результат, чужой ключ отдаёт 409, а от гонки двух параллельных запросов защищает частичный уникальный индекс — приложение может проверить и не успеть, база не может
Совет
Иногда ключ не нужен: если у операции есть естественный идентификатор от клиента — номер заказа, externalId вакансии, — достаточно уникального индекса по нему и ON CONFLICT DO NOTHING. Ключ нужен там, где естественного идентификатора нет
10. Dead Letter Queue
Одно битое сообщение не обработается никогда, а консьюмер падает на нём снова и снова. Оффсет не двигается — вся партиция стоит за ним в очереди
После N попыток сообщение уезжает в отдельный топик вместе с причиной. Очередь разблокирована, сообщение сохранено для разбора
Такое сообщение называют ядовитым: пришло без обязательного поля, схема разъехалась, данные битые. Повторы его не вылечат — оно не станет валидным от того, что вы попробуете ещё раз
Поэтому бесконечный ретрай здесь — это не устойчивость, а вечная остановка партиции. Лечится одним способом: убрать сообщение с дороги, но не потерять
retry → DLT → алерт → переигровка
Одно битое сообщение больше не блокирует партицию
В партиции лежит сообщение без номера телефона. Обработчик падает на нём, оффсет не двигается — и все следующие сообщения этой партиции стоят за ним в очереди
- N попыток с бэкоффом, потом перенос в
topic.DLT - Вместе с телом переносятся причина, стектрейс, исходный топик, партиция и оффсет
- Счётчик сообщений в DLT — обязательная метрика, а не приятное дополнение
- Алерт на первое же сообщение — в норме очередь пуста
- После починки сообщения переигрываются обратно в основной топик
Памятка
Продакшн-кейс
SMS-воркер спотыкался на сообщениях без номера телефона. Три попытки, перенос в notifications.DLT, алерт дежурному через минуту. Партиция разблокирована, отправка остальным идёт как обычно, а битые сообщения ждут разбора
Не делай так
Грабли
DLQ без алерта — это молчаливая свалка. Сообщения тихо копятся неделями, никто их не смотрит, и обнаруживается это от пользователя, который третий месяц не получает уведомления. Если на очередь нет метрики и правила «больше нуля — буди», значит, вы не отложили проблему, а спрятали её
11. Pub/Sub в Redis
WebSocket пользователя открыт к одному инстансу, а событие для него обработал другой. Написать в чужой сокет он физически не может
Инстанс публикует сообщение в канал Redis, его получают все, доставляет тот, у кого есть соединение. Карту «кто где» держать не нужно
Соединения раскиданы по подам как попало — пользователь подключился туда, куда его завёл балансировщик. Знать эту раскладку невозможно и не нужно: вместо адресации получателя мы делаем рассылку, а лишние инстансы просто молча игнорируют
PUBLISH → все инстансы → доставляет один
Сокет пользователя висит на другом инстансе
Событие для пользователя 42 обработал инстанс API #1. Только вот WebSocket этого пользователя открыт к API #2 — инстанс с событием физически не может ему написать
PUBLISHдоставляется только тем, кто подписан прямо сейчас- Подписчик был отключён — сообщение для него не существовало, повтора не будет
- Ни очереди, ни оффсетов, ни подтверждений — доставка не гарантирована в принципе
- Зато задержка в миллисекундах и никакой отдельной инфраструктуры
Памятка
Продакшн-кейс
Колокольчик с непрочитанными обновляется через канал user:{id}. Событие приходит на любой инстанс, публикуется в Redis, доставляет тот, где висит сокет. Само уведомление при этом уже лежит в PostgreSQL — если сообщение потеряется, пользователь увидит его при следующем открытии экрана
Важно
Где проходит граница
Pub/Sub годится для того, что не жалко потерять: живые счётчики, presence, инвалидация кеша. Всё, что обязано дойти, идёт через Kafka или Redis Streams с подтверждением. Признак ошибки в проектировании простой: если для Pub/Sub понадобилось «а если не дошло, то повторить» — вы выбрали не тот инструмент
12. Saga и виды саг
Оформление подписки трогает три сервиса, у каждого своя база. Общей транзакции между ними не существует — списали деньги, а доступ не открылся
Цепочка локальных транзакций, у каждой есть компенсация. Отказ на последнем шаге разворачивает предыдущие обратными действиями
Двухфазный коммит существует, но в микросервисах от него отказались: он блокирует ресурсы на всё время согласования и умирает вместе с координатором. Сага честно признаёт другое — атомарности не будет, будет последовательность шагов, каждый из которых коммитится сам по себе
Компенсация — это не rollback. Откатывать уже нечего: чужой сервис давно закоммитил своё. Это новое бизнес-действие, обратное по смыслу — возврат вместо отмены списания
шаги вперёд → компенсации назад
Оплата прошла, письмо не ушло — откатываем по шагам
Распределённой транзакции на три сервиса не существует, поэтому сага делает шаги по очереди. Первый — списать деньги, он подтверждён
Оркестрация
Отдельный компонент знает весь сценарий, вызывает шаги по очереди и хранит состояние саги. Плюс — сценарий целиком читается в одном файле, и по состоянию видно, где именно всё встало. Минус — оркестратор становится центром, от которого зависят все, и его отказ останавливает процесс
Хореография
Центра нет: каждый сервис слушает событие предыдущего и публикует своё. Плюс — связность ниже, добавить шаг проще. Минус — сценарий целиком не написан нигде, и как только шагов становится больше трёх, вопрос «почему подписка активна, а денег не списали» превращается в раскопки по логам всех участников
Памятка
Продакшн-кейс
Деньги списались, доступ открылся, письмо не ушло — адрес не существует, повторы не помогут. Сага пошла назад: закрыла доступ, вернула деньги. В выписке остались обе операции, списание и возврат, и это правильно: в реальном мире событие уже произошло, его можно только уравновесить
Важно
Компенсация тоже падает
Возврат денег может не пройти с первого раза. Компенсациям нужны повторы, идемпотентность и предел попыток, после которого поднимается человек. Сага, у которой компенсация «просто вызывается», разваливается на первом же таймауте платёжного шлюза
Совет
Порядок шагов выбирается так, чтобы необратимое было последним. Сначала то, что можно отменить, — резерв, блокировка средств; в конце то, что отменить нельзя, — отправка письма или физическая отгрузка
Что дальше
Двенадцать паттернов выше отвечают на один вопрос: как система переживает отказы и не теряет данные. Это половина разговора на собеседовании
Вторая половина — про то, как всё это живёт в кластере: как сервисы находят друг друга, как выкатываются без потерь, как за ними смотреть и как менять контракты, ничего не сломав
СсылкаЧасть 2. Эксплуатация: кластер, наблюдаемость, измененияService discovery, sidecar, health check, graceful shutdown, распределённые локи, API-ключи, фича-флаги, контракты, метрики и трассировка