> Как снять бронь товара при отмене заказа (Go)

Уровень: senior · Роль: backend · Категория: Технические вопросы

Компании: sferaplatform.ru

Стек: Go

> Пример ответа

Короткий ответ

Снятие брони при отмене заказа - это компенсационная транзакция в рамках распределённой системы. В Go это реализуется через идемпотентный вызов сервиса инвентаря с передачей идентификатора брони и причины отмены. Ключевые моменты: обработка частичных сбоев, повторные попытки с экспоненциальной задержкой и гарантия того, что операция выполнится ровно один раз. Если бронь уже была конвертирована в списание, снятие не выполняется - вместо этого запускается возврат.

Подробное объяснение

Бронь товара - это временное резервирование единицы inventory на определённый срок. При отмене заказа нужно вернуть эту единицу в доступный пул. Основная сложность - согласованность между сервисом заказов и сервисом инвентаря в условиях сетевых сбоев.

Архитектурно это выглядит так: сервис заказов отправляет команду releaseReservation в inventory service. Команда должна быть идемпотентной - повторная отправка не должна приводить к двойному освобождению. Для этого используется идентификатор брони как ключ идемпотентности.

Важно различать состояния брони:

  • active - бронь действует, товар зарезервирован
  • released - бронь снята, товар доступен
  • converted - бронь превратилась в фактическое списание (заказ оплачен и отгружен)

Если заказ отменяется после конвертации, снятие брони не применяется - вместо этого запускается процесс возврата товара, который управляется отдельным workflow.

Временные окна тоже имеют значение: если бронь уже истекла по таймауту, повторное снятие должно быть no-op, а не ошибкой.

На практике

В реальном Go-сервисе это обычно реализуется через:

  1. Outbox pattern - событие об отмене заказа пишется в таблицу outbox в той же транзакции, что и изменение статуса заказа. Отдельный воркер читает outbox и отправляет команды в inventory service.

  2. Сага - оркестратор управляет последовательностью компенсационных действий. При отмене заказа сага вызывает шаг releaseReservation, и если он падает, повторяет с retry policy.

  3. Идемпотентный ключ - в запросе к inventory service передаётся reservationID и operationID. Сервис хранит выполненные операции и при повторном запросе возвращает прежний результат.

Таймауты и retry: обычно используется exponential backoff с jitter, максимум 3-5 попыток. Если после всех попыток снятие не удалось, создаётся компенсационное событие в DLQ (dead letter queue) для ручного разбора.

Пример кода

GO
type ReleaseReservationRequest struct {
ReservationID string
OperationID string
Reason 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-op
case 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 nil
default:
return fmt.Errorf("unexpected status: %s", reservation.Status)
}
}

Как отвечать на собеседовании

Начни с постановки проблемы: распределённая система, сетевая ненадёжность, нужна идемпотентность. Затем опиши два уровня: бизнес-логика (статусы брони) и инфраструктурный (retry, outbox, сага). Покажи, что понимаешь разницу между снятием брони и возвратом товара. Упомяни, что в высоконагруженных системах важно минимизировать блокировки - используй оптимистичные транзакции или CAS-подобные операции. Если спросят про согласованность, говори про eventual consistency и компенсационные действия, а не про распределённые транзакции с двухфазным коммитом - они редко применимы.

Что проверяет интервьюер

  • Понимание идемпотентности и умение её реализовать
  • Знание паттернов распределённых транзакций: сага, outbox, компенсация
  • Умение работать с состояниями и переходами между ними
  • Понимание trade-off между строгой и конечной согласованностью
  • Навыки проектирования API: как передавать ключи идемпотентности, как обрабатывать ошибки

Типичные ошибки

  • Предлагать распределённую транзакцию с 2PC - это антипаттерн для микросервисов
  • Игнорировать идемпотентность: повторный запрос приводит к двойному освобождению товара
  • Не различать снятие брони и возврат товара - это разные процессы с разными гарантиями
  • Считать, что таймаут запроса означает провал операции - операция могла выполниться, но ответ потерялся
  • Не обрабатывать случай, когда бронь уже истекла по таймауту - повторное снятие должно быть no-op, а не ошибкой
  • Забывать про мониторинг и DLQ для необработанных компенсаций

> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?

Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью