> Как устроен оркестратор, управляющий задачами для платежей (Go)

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

Компании: InDrive

Стек: Go

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

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

Оркестратор платежей - это сервис, который управляет жизненным циклом платёжной транзакции: принимает запрос, разбивает его на шаги (авторизация, захват средств, возврат), вызывает внешние платёжные провайдеры и обновляет состояние задачи. В Go это обычно реализуется как state machine с явным хранением состояний в БД, очередью задач (например, Kafka или RabbitMQ) для асинхронных шагов и идемпотентными обработчиками. Ключевые принципы: идемпотентность, retry с exponential backoff, обработка частичных сбоев и компенсационные действия.

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

Оркестратор задач для платежей решает три основные проблемы: координацию множества шагов, устойчивость к сбоям и согласованность данных.

Архитектура обычно состоит из:

  • API-слой - принимает запросы на создание платежа
  • Ядро оркестратора - содержит state machine и бизнес-логику переходов
  • Хранилище состояний - PostgreSQL или аналогичная БД с транзакциями
  • Очередь сообщений - для асинхронных шагов и retry
  • Адаптеры провайдеров - интерфейсы к внешним платёжным системам

State machine - центральный элемент. Каждый платёж проходит через состояния: created, authorizing, authorized, capturing, captured, failed, refunded. Переходы между состояниями - это отдельные обработчики, которые выполняют конкретные действия и меняют состояние атомарно.

Обработка сбоев строится на трёх уровнях:

  1. Retry на уровне шага - если провайдер вернул временную ошибку, шаг повторяется с exponential backoff
  2. Dead letter queue - если шаг не выполнился после N попыток, задача отправляется в DLQ для ручного разбора
  3. Компенсация - если последующий шаг не может быть выполнен, оркестратор запускает обратные операции (например, refund при неудачном capture)

Идемпотентность критична: повторная обработка одного и того же события не должна создавать дубликаты. Для этого используется уникальный ключ идемпотентности (например, payment_id + step_name), по которому проверяется, не был ли шаг уже выполнен.

Согласованность достигается через транзакции в БД: обновление состояния и запись события в outbox-таблицу происходят атомарно. Отдельный процесс читает outbox и публикует события в очередь.

На практике

В Go оркестратор обычно строится на следующих паттернах:

  • Интерфейс шага: type Step interface { Execute(ctx context.Context, payment *Payment) error }
  • Реестр шагов: map из имени шага в его реализацию
  • Диспетчер: читает задачи из очереди, определяет текущее состояние, выбирает следующий шаг и выполняет его
  • Хранилище: репозиторий с методами GetPayment, UpdateState, AppendEvent

Типичный flow обработки задачи:

  1. Получить сообщение из очереди (например, payment.created)
  2. Загрузить платеж из БД по ID
  3. Проверить текущее состояние и что шаг ещё не выполнялся
  4. Выполнить действие (вызвать провайдера)
  5. В транзакции обновить состояние и записать событие в outbox
  6. Опубликовать следующее событие в очередь
  7. Подтвердить обработку сообщения (ack)

Для retry используется библиотека типа github.com/avast/retry-go или собственная обёртка с jitter.

Пример кода

GO
type Payment struct {
ID string
State string
Amount int64
Currency string
Provider string
Attempts int
UpdatedAt time.Time
}
type Step interface {
Name() string
Execute(ctx context.Context, p *Payment) error
// NextState возвращает состояние после успешного выполнения
NextState() string
}
type Orchestrator struct {
repo PaymentRepository
steps map[string]Step
queue Queue
retry RetryPolicy
}
func (o *Orchestrator) ProcessMessage(ctx context.Context, msg Message) error {
payment, err := o.repo.GetByID(ctx, msg.PaymentID)
if err != nil {
return err
}
step, ok := o.steps[payment.State]
if !ok {
return fmt.Errorf("no step for state %s", payment.State)
}
// идемпотентность: проверяем, не выполнялся ли уже шаг
if msg.EventID == payment.LastEventID {
return nil
}
err = o.retry.Do(ctx, func() error {
return step.Execute(ctx, payment)
})
if err != nil {
return o.handleFailure(ctx, payment, step, err)
}
// атомарное обновление состояния и запись события
return o.repo.UpdateState(ctx, payment.ID, step.NextState(), msg.EventID)
}
func (o *Orchestrator) handleFailure(ctx context.Context, p *Payment, step Step, err error) error {
if p.Attempts >= o.retry.MaxAttempts {
// компенсация: например, перевод в failed и запуск refund
return o.compensate(ctx, p, err)
}
// повторная постановка в очередь с задержкой
return o.queue.PublishDelayed(ctx, p.ID, o.retry.Backoff(p.Attempts))
}

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

Начните с высокоуровневого описания: оркестратор - это state machine + очередь + хранилище. Затем переходите к деталям: как обеспечивается идемпотентность, как обрабатываются сбои, как гарантируется согласованность. Обязательно упомяните trade-off между orchestration и choreography - в платежах обычно выбирают оркестрацию, потому что нужен явный контроль над каждым шагом.

Приведите конкретный пример из практики: как обрабатывается ситуация, когда провайдер завис на авторизации, или как выполняется retry при временной недоступности. Покажите, что понимаете проблемы распределённых систем: частичные сбои, потеря сообщений, дубликаты.

Если спросят про масштабирование - расскажите про партиционирование очереди по payment_id, шардирование БД по ключу платежа, и что оркестратор должен быть stateless, чтобы горизонтально масштабироваться.

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

Интервьюер оценивает:

  • Понимание паттернов распределённых систем: saga, outbox, idempotency, retry
  • Умение проектировать state machine и описывать переходы
  • Знание Go-специфики: горутины для параллельной обработки, контексты, обработка ошибок
  • Практический опыт с очередями и БД в контексте платежей
  • Способность рассуждать о компромиссах: например, почему не используется двухфазный коммит, а вместо этого saga

Главное - показать, что вы не просто знаете теорию, а понимаете, какие проблемы возникают в реальных платёжных системах и как их решать.

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

  • Игнорирование идемпотентности - кандидат не упоминает, что повторная обработка сообщения может привести к двойному списанию
  • Отсутствие компенсационных действий - не говорит, что делать, если capture не удался после авторизации
  • Слишком простая модель retry - предлагает бесконечные повторы без backoff и DLQ
  • Не различает синхронные и асинхронные шаги - пытается всё делать в одном запросе, хотя часть операций должна быть асинхронной
  • Забывает про outbox pattern - не объясняет, как гарантировать, что событие в очереди не потеряется при сбое после записи в БД
  • Путает оркестрацию и хореографию - не может объяснить, почему для платежей выбрана оркестрация
  • Не учитывает частичные сбои провайдеров - предполагает, что внешний сервис всегда отвечает корректно

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

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