Задача 8 из 10
Оплата заказа
Спроектируйте оформление и оплату заказа: списать деньги, зарезервировать товар и отправить чек — и не потерять и не задвоить ни одно из этих действий.
Функциональные требования
- Создание заказа и его оплата
- Резервирование товара на складе
- Возврат средств при отмене
- История операций по заказу
Нефункциональные требования
- 500 заказов/с в пик
- Деньги нельзя списать дважды и нельзя потерять
- Платёжный провайдер отвечает от 200 мс до 30 секунд, иногда таймаутит
- Заказ должен прийти в консистентное состояние даже после падения любого сервиса
Нарисуйте архитектуру
Не гонитесь за красотой: на собеседовании от схемы нужно, чтобы по ней было видно путь запроса и где лежат данные. Сначала нарисуйте сами — эталон и разбор ниже.
Блок из палитры — добавить. Тащите мышкой или пальцем. Двойной клик по названию — переименовать. «Связь» — щёлкнуть по одному блоку, потом по другому: получится стрелка.
Схема сохраняется в этом браузере — вкладку можно закрыть и вернуться позже.
1 С чего начать, если ступор
- Начните с главного: распределённой транзакции между вашей БД и банком не существует.
- Идемпотентность: клиент присылает ключ запроса, повторный вызов не должен списать второй раз.
- Сага против двухфазного коммита — объясните, почему 2PC здесь не применяют.
- Outbox: как гарантировать, что событие уйдёт ровно тогда, когда транзакция закоммитилась.
- Вебхук провайдера придёт дважды и не по порядку — решите это заранее, а не потом.
2 Эталонная схема
Заказ и событие о нём пишутся одной транзакцией в БД (outbox), а публикует событие отдельный воркер — так оно не потеряется и не уйдёт раньше коммита. Дальше сага: оплата, резерв, чек — каждый шаг со своей компенсацией, а не одна общая транзакция на всех.
Это один из рабочих вариантов, а не единственно верный. Если у вас иначе, но вы можете объяснить почему — на собеседовании это ровно то, что нужно.
3 Разбор: что должно прозвучать
Идемпотентность
- Ключ идемпотентности приходит от клиента и хранится с уникальным индексом
- Повтор с тем же ключом возвращает результат первой попытки, а не создаёт второй заказ
- Ретрай по таймауту — это норма: клиент не знает, дошёл ли его запрос
Почему не двухфазный коммит
- Банк не участвует в вашей транзакции — 2PC физически некуда протянуть
- Координатор 2PC становится единой точкой отказа и держит блокировки на время ожидания
- Отсюда сага: последовательность локальных транзакций плюс компенсации
Сага и компенсации
- Каждый шаг имеет обратное действие: списали деньги → возврат, зарезервировали → снять резерв
- Компенсация тоже может упасть, поэтому она идемпотентна и повторяема
- Заказ живёт как конечный автомат со статусами, а не как набор флагов
Outbox
- «Сохранили в БД и отправили в очередь» теряет событие, если процесс упал между этими шагами
- Событие пишется в ту же транзакцию, что и заказ; отдельный воркер публикует его и помечает отправленным
- Публикация at-least-once, поэтому потребители обязаны быть идемпотентными
Вебхуки провайдера
- Придут дважды, могут прийти не по порядку и по заказу, который вы ещё не успели создать
- Проверка подписи обязательна: это внешний вход в вашу денежную логику
- Не доверяйте только вебхуку — нужен и опрос статуса платежа по таймауту
Куда копать дальше, если спросят
- Регулярная сверка с провайдером: расхождения находятся именно так
- Деньги хранятся в целых копейках, никаких float
- Аудит-лог всех операций: кто, когда и на каком основании изменил статус
- Частичные возвраты и заказы, где часть товара уже отгружена
Следующая задача: Аналитика в реальном времени
100 000 событий в секунду и требование считать уникальных. Задача про то, где точность не стоит своих денег.