← все задачи

Задача 8 из 10

Оплата заказа

Спроектируйте оформление и оплату заказа: списать деньги, зарезервировать товар и отправить чек — и не потерять и не задвоить ни одно из этих действий.

Уровень: Продвинутый На собеседовании: 45–60 минут идемпотентностьoutboxсага и компенсации

Функциональные требования

  • Создание заказа и его оплата
  • Резервирование товара на складе
  • Возврат средств при отмене
  • История операций по заказу

Нефункциональные требования

  • 500 заказов/с в пик
  • Деньги нельзя списать дважды и нельзя потерять
  • Платёжный провайдер отвечает от 200 мс до 30 секунд, иногда таймаутит
  • Заказ должен прийти в консистентное состояние даже после падения любого сервиса

Нарисуйте архитектуру

Не гонитесь за красотой: на собеседовании от схемы нужно, чтобы по ней было видно путь запроса и где лежат данные. Сначала нарисуйте сами — эталон и разбор ниже.

Блок из палитры — добавить. Тащите мышкой или пальцем. Двойной клик по названию — переименовать. «Связь» — щёлкнуть по одному блоку, потом по другому: получится стрелка.

Схема сохраняется в этом браузере — вкладку можно закрыть и вернуться позже.

1 С чего начать, если ступор
  • Начните с главного: распределённой транзакции между вашей БД и банком не существует.
  • Идемпотентность: клиент присылает ключ запроса, повторный вызов не должен списать второй раз.
  • Сага против двухфазного коммита — объясните, почему 2PC здесь не применяют.
  • Outbox: как гарантировать, что событие уйдёт ровно тогда, когда транзакция закоммитилась.
  • Вебхук провайдера придёт дважды и не по порядку — решите это заранее, а не потом.
2 Эталонная схема

Заказ и событие о нём пишутся одной транзакцией в БД (outbox), а публикует событие отдельный воркер — так оно не потеряется и не уйдёт раньше коммита. Дальше сага: оплата, резерв, чек — каждый шаг со своей компенсацией, а не одна общая транзакция на всех.

Это один из рабочих вариантов, а не единственно верный. Если у вас иначе, но вы можете объяснить почему — на собеседовании это ровно то, что нужно.

3 Разбор: что должно прозвучать

Идемпотентность

  • Ключ идемпотентности приходит от клиента и хранится с уникальным индексом
  • Повтор с тем же ключом возвращает результат первой попытки, а не создаёт второй заказ
  • Ретрай по таймауту — это норма: клиент не знает, дошёл ли его запрос

Почему не двухфазный коммит

  • Банк не участвует в вашей транзакции — 2PC физически некуда протянуть
  • Координатор 2PC становится единой точкой отказа и держит блокировки на время ожидания
  • Отсюда сага: последовательность локальных транзакций плюс компенсации

Сага и компенсации

  • Каждый шаг имеет обратное действие: списали деньги → возврат, зарезервировали → снять резерв
  • Компенсация тоже может упасть, поэтому она идемпотентна и повторяема
  • Заказ живёт как конечный автомат со статусами, а не как набор флагов

Outbox

  • «Сохранили в БД и отправили в очередь» теряет событие, если процесс упал между этими шагами
  • Событие пишется в ту же транзакцию, что и заказ; отдельный воркер публикует его и помечает отправленным
  • Публикация at-least-once, поэтому потребители обязаны быть идемпотентными

Вебхуки провайдера

  • Придут дважды, могут прийти не по порядку и по заказу, который вы ещё не успели создать
  • Проверка подписи обязательна: это внешний вход в вашу денежную логику
  • Не доверяйте только вебхуку — нужен и опрос статуса платежа по таймауту

Куда копать дальше, если спросят

  • Регулярная сверка с провайдером: расхождения находятся именно так
  • Деньги хранятся в целых копейках, никаких float
  • Аудит-лог всех операций: кто, когда и на каком основании изменил статус
  • Частичные возвраты и заказы, где часть товара уже отгружена

Следующая задача: Аналитика в реальном времени

100 000 событий в секунду и требование считать уникальных. Задача про то, где точность не стоит своих денег.