> Как снять бронь товара при отмене заказа (Go)
Уровень: senior · Роль: backend · Категория: Технические вопросы
Компании: sferaplatform.ru
Стек: Go
> Пример ответа
Короткий ответ
Снятие брони при отмене заказа - это компенсационная транзакция в рамках распределённой системы. В Go это реализуется через идемпотентный вызов сервиса инвентаря с передачей идентификатора брони и причины отмены. Ключевые моменты: обработка частичных сбоев, повторные попытки с экспоненциальной задержкой и гарантия того, что операция выполнится ровно один раз. Если бронь уже была конвертирована в списание, снятие не выполняется - вместо этого запускается возврат.
Подробное объяснение
Бронь товара - это временное резервирование единицы inventory на определённый срок. При отмене заказа нужно вернуть эту единицу в доступный пул. Основная сложность - согласованность между сервисом заказов и сервисом инвентаря в условиях сетевых сбоев.
Архитектурно это выглядит так: сервис заказов отправляет команду releaseReservation в inventory service. Команда должна быть идемпотентной - повторная отправка не должна приводить к двойному освобождению. Для этого используется идентификатор брони как ключ идемпотентности.
Важно различать состояния брони:
active- бронь действует, товар зарезервированreleased- бронь снята, товар доступенconverted- бронь превратилась в фактическое списание (заказ оплачен и отгружен)
Если заказ отменяется после конвертации, снятие брони не применяется - вместо этого запускается процесс возврата товара, который управляется отдельным workflow.
Временные окна тоже имеют значение: если бронь уже истекла по таймауту, повторное снятие должно быть no-op, а не ошибкой.
На практике
В реальном Go-сервисе это обычно реализуется через:
-
Outbox pattern - событие об отмене заказа пишется в таблицу outbox в той же транзакции, что и изменение статуса заказа. Отдельный воркер читает outbox и отправляет команды в inventory service.
-
Сага - оркестратор управляет последовательностью компенсационных действий. При отмене заказа сага вызывает шаг
releaseReservation, и если он падает, повторяет с retry policy. -
Идемпотентный ключ - в запросе к inventory service передаётся
reservationIDиoperationID. Сервис хранит выполненные операции и при повторном запросе возвращает прежний результат.
Таймауты и retry: обычно используется exponential backoff с jitter, максимум 3-5 попыток. Если после всех попыток снятие не удалось, создаётся компенсационное событие в DLQ (dead letter queue) для ручного разбора.
Пример кода
GOtype ReleaseReservationRequest struct {ReservationID stringOperationID stringReason string}func (s *InventoryService) ReleaseReservation(ctx context.Context, req *ReleaseReservationRequest) error {// идемпотентность: проверяем, не выполняли ли уже эту операциюif _, err := s.store.GetOperation(ctx, req.OperationID); err == nil {return nil // уже выполнено}reservation, err := s.store.GetReservation(ctx, req.ReservationID)if err != nil {return fmt.Errorf("get reservation: %w", err)}switch reservation.Status {case ReservationStatusReleased:return nil // уже снята - no-opcase ReservationStatusConverted:return ErrReservationConverted // нужен возврат, не снятиеcase ReservationStatusActive:// атомарно: меняем статус и записываем операциюerr := s.store.WithTx(ctx, func(tx *sql.Tx) error {if err := tx.UpdateReservationStatus(ctx, req.ReservationID, ReservationStatusReleased); err != nil {return err}return tx.InsertOperation(ctx, req.OperationID, req.ReservationID)})if err != nil {return fmt.Errorf("release reservation: %w", err)}return nildefault:return fmt.Errorf("unexpected status: %s", reservation.Status)}}
Как отвечать на собеседовании
Начни с постановки проблемы: распределённая система, сетевая ненадёжность, нужна идемпотентность. Затем опиши два уровня: бизнес-логика (статусы брони) и инфраструктурный (retry, outbox, сага). Покажи, что понимаешь разницу между снятием брони и возвратом товара. Упомяни, что в высоконагруженных системах важно минимизировать блокировки - используй оптимистичные транзакции или CAS-подобные операции. Если спросят про согласованность, говори про eventual consistency и компенсационные действия, а не про распределённые транзакции с двухфазным коммитом - они редко применимы.
Что проверяет интервьюер
- Понимание идемпотентности и умение её реализовать
- Знание паттернов распределённых транзакций: сага, outbox, компенсация
- Умение работать с состояниями и переходами между ними
- Понимание trade-off между строгой и конечной согласованностью
- Навыки проектирования API: как передавать ключи идемпотентности, как обрабатывать ошибки
Типичные ошибки
- Предлагать распределённую транзакцию с 2PC - это антипаттерн для микросервисов
- Игнорировать идемпотентность: повторный запрос приводит к двойному освобождению товара
- Не различать снятие брони и возврат товара - это разные процессы с разными гарантиями
- Считать, что таймаут запроса означает провал операции - операция могла выполниться, но ответ потерялся
- Не обрабатывать случай, когда бронь уже истекла по таймауту - повторное снятие должно быть no-op, а не ошибкой
- Забывать про мониторинг и DLQ для необработанных компенсаций
> Похожие задачи по backend
Как обнаруживать и реагировать на проблемы с отменой брони
Как обрабатывать ошибки при отмене брони на складе
Как гарантировать, что заказ не будет выполнен без исполнителя после резерва товара
Напиши функцию на Go, которая принимает слайс каналов int и возвращает канал с суммами чисел из каждого канала
> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?
Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью