> Как устроен оркестратор, управляющий задачами для платежей (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. Переходы между состояниями - это отдельные обработчики, которые выполняют конкретные действия и меняют состояние атомарно.
Обработка сбоев строится на трёх уровнях:
- Retry на уровне шага - если провайдер вернул временную ошибку, шаг повторяется с exponential backoff
- Dead letter queue - если шаг не выполнился после N попыток, задача отправляется в DLQ для ручного разбора
- Компенсация - если последующий шаг не может быть выполнен, оркестратор запускает обратные операции (например, refund при неудачном capture)
Идемпотентность критична: повторная обработка одного и того же события не должна создавать дубликаты. Для этого используется уникальный ключ идемпотентности (например, payment_id + step_name), по которому проверяется, не был ли шаг уже выполнен.
Согласованность достигается через транзакции в БД: обновление состояния и запись события в outbox-таблицу происходят атомарно. Отдельный процесс читает outbox и публикует события в очередь.
На практике
В Go оркестратор обычно строится на следующих паттернах:
- Интерфейс шага:
type Step interface { Execute(ctx context.Context, payment *Payment) error } - Реестр шагов: map из имени шага в его реализацию
- Диспетчер: читает задачи из очереди, определяет текущее состояние, выбирает следующий шаг и выполняет его
- Хранилище: репозиторий с методами
GetPayment,UpdateState,AppendEvent
Типичный flow обработки задачи:
- Получить сообщение из очереди (например,
payment.created) - Загрузить платеж из БД по ID
- Проверить текущее состояние и что шаг ещё не выполнялся
- Выполнить действие (вызвать провайдера)
- В транзакции обновить состояние и записать событие в outbox
- Опубликовать следующее событие в очередь
- Подтвердить обработку сообщения (ack)
Для retry используется библиотека типа github.com/avast/retry-go или собственная обёртка с jitter.
Пример кода
GOtype Payment struct {ID stringState stringAmount int64Currency stringProvider stringAttempts intUpdatedAt time.Time}type Step interface {Name() stringExecute(ctx context.Context, p *Payment) error// NextState возвращает состояние после успешного выполненияNextState() string}type Orchestrator struct {repo PaymentRepositorysteps map[string]Stepqueue Queueretry 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 и запуск refundreturn 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 - не объясняет, как гарантировать, что событие в очереди не потеряется при сбое после записи в БД
- Путает оркестрацию и хореографию - не может объяснить, почему для платежей выбрана оркестрация
- Не учитывает частичные сбои провайдеров - предполагает, что внешний сервис всегда отвечает корректно
> Похожие задачи по Go
Почему нельзя заменить канал в Go обычным слайсом сообщений?
Зачем нужен отдельный сервис на Go для оркестратора
Что такое MVCC в PostgreSQL
Что такое b-tree индекс и что означает буква B
> Похожие задачи по backend
Почему нельзя заменить канал в Go обычным слайсом сообщений?
Зачем нужен отдельный сервис на Go для оркестратора
Что такое MVCC в PostgreSQL
Что такое b-tree индекс и что означает буква B
> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?
Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью