Перейти к содержанию

SAGA

Saga — это способ выполнить длинную бизнес-операцию как последовательность локальных транзакций в разных сервисах или модулях. Каждый шаг делает свой локальный commit, а если следующий шаг не удался, предыдущие шаги не откатываются общей ACID-транзакцией. Вместо этого выполняются компенсирующие действия.

Saga = local transactions + coordination + compensations

Главная цель Saga: не глобальный rollback, а приведение системы к согласованному бизнес-состоянию.

Saga появляется там, где:

  • каждый сервис владеет своей БД;
  • общий XA/2PC слишком тяжелый или плохо подходит архитектуре;
  • часть шагов асинхронна;
  • есть внешние API и необратимые побочные эффекты;
  • автономность и доступность важнее, чем глобальный ACID commit.

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


Compensation != Rollback

Это самая важная мысль в теме Saga.

Rollback:

  • работает внутри локальной транзакции;
  • отменяет незакоммиченные изменения;
  • не отменяет уже завершенный внешний эффект.

Compensation:

  • это новое бизнес-действие;
  • оно логически нейтрализует прошлый шаг;
  • оно не делает вид, что прошлого шага "никогда не было".

Примеры:

  • reserve inventory -> release inventory
  • charge payment -> refund payment
  • create shipment -> cancel shipment

Почему Не 2PC / XA

Есть два типовых подхода к распределенной бизнес-операции.

2PC / XA:

  • дают строгую атомарность;
  • сложны во внедрении;
  • плохо ложатся на микросервисы;
  • держат блокировки дольше;
  • усиливают связанность;
  • создают проблемы для доступности и производительности;
  • не везде нормально поддерживаются брокерами и интеграциями.

Saga:

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

Идея Saga не в том, чтобы "сымитировать XA под другим именем", а в том, чтобы принять другую модель согласованности и управления ошибками.


Из Чего Состоит Saga

В нормальном дизайне у Saga есть несколько обязательных частей.

Шаги:

  • каждый шаг — отдельная локальная транзакция;
  • шаг должен быть атомарным в пределах своего сервиса.

Компенсации:

  • для обратимых шагов обычно есть compensating action;
  • компенсация тоже должна проектироваться как отдельное бизнес-действие.

Контекст Saga:

  • saga_id
  • status
  • current_step
  • payload/context
  • correlation_id
  • история выполнения
  • информация о retry и last error

Координация:

  • кто решает, какой шаг запускать следующим;
  • кто решает, когда делать retry;
  • кто запускает компенсации;
  • кто определяет, что сага завершилась успешно или неуспешно.

Два основных варианта координации: choreography и orchestration.


Как Saga Работает На Практике

Пример: оформление заказа. Участвуют Order Service, Inventory Service, Payment Service, Shipping Service.

Успешный сценарий:

1. Order Service -> create order (PENDING)
2. Inventory Service -> reserve items
3. Payment Service -> charge
4. Shipping Service -> create shipment
5. Order Service -> mark order (CONFIRMED)

Каждый шаг:

  • делает локальную транзакцию;
  • коммитит ее;
  • инициирует следующий шаг.

Сценарий с ошибкой:

shipping failed
=> refund payment
=> release inventory
=> cancel order

То есть откат идет не через глобальный rollback, а через цепочку compensating actions в обратном порядке.


Классические Типы Шагов: Compensatable, Pivot, Retryable

На senior-уровне полезно делить шаги саги не просто на "обычные" и "компенсации", а на три классических типа.

Compensatable step:

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

Примеры:

  • создать черновик заказа;
  • поставить резерв на товар;
  • зарезервировать лимит или слот.

Pivot step:

  • это точка невозврата;
  • после успешного выполнения pivot уже нельзя просто "вернуться в исходное состояние";
  • после pivot оставшиеся шаги обычно должны либо дойти до конца, либо переводиться в отдельный recovery flow.

Примеры:

  • окончательное списание денег;
  • фиксация операции во внешней системе, где отмена дорогая или асинхронная;
  • юридически значимое подтверждение, после которого начинается внешний side effect.

Retryable step:

  • это шаг после pivot, который не хочется компенсировать;
  • его проектируют так, чтобы он в итоге завершился успехом через retry;
  • если он не завершается быстро, обычно нужен фоновый recovery process.

Примеры:

  • доставка уведомления во внутреннюю систему;
  • создание внутренних projection или read model;
  • запись во вторичную систему, которая должна в итоге догнаться.

Практическая мысль:

  • до pivot лучше ставить обратимые действия;
  • pivot должен быть выбран очень осознанно;
  • после pivot шаги должны быть максимально надежными и идемпотентными.

Не каждая реальная сага идеально раскладывается на эти три класса, но эта модель очень полезна для дизайна и code review.


Choreography И Orchestration

У Saga есть два главных способа координации.

Choreography — это модель, в которой нет одного центрального координатора. Сервисы публикуют события, слушают события друг друга и сами понимают, что делать дальше.

Пример:

Order Service publishes OrderCreated
Inventory Service listens and publishes InventoryReserved
Payment Service listens and publishes PaymentCharged
Shipping Service listens and publishes ShipmentCreated

Если что-то падает, сервис публикует failure event, а другие сервисы запускают свои компенсации.

Плюсы choreography:

  • хорошо ложится на event-driven architecture;
  • сервисы более автономны;
  • нет отдельного orchestration service;
  • простой старт для небольших flow.

Минусы choreography:

  • логика процесса размазана по многим сервисам;
  • тяжело увидеть весь flow в одном месте;
  • сложнее дебажить;
  • выше риск event spaghetti.

Orchestration — это модель, в которой есть центральный оркестратор. Он знает модель процесса, запускает шаги, ждет ответы, принимает решения о retry и compensation, держит состояние саги.

Пример:

Client -> Saga Orchestrator

orchestrator -> Order Service: createOrder
orchestrator -> Inventory Service: reserve
orchestrator -> Payment Service: charge
orchestrator -> Shipping Service: createShipment

Если shipping упал:

orchestrator -> Payment Service: refund
orchestrator -> Inventory Service: release
orchestrator -> Order Service: cancel

Плюсы orchestration:

  • весь flow виден в одном месте;
  • проще дебажить;
  • проще менять сценарий;
  • легче делать timeout / retry policy;
  • лучше подходит для длинных процессов.

Минусы orchestration:

  • появляется центральный компонент;
  • он может стать слишком сложным;
  • при плохом дизайне получается god service.

Практическое правило:

  • маленький и естественно событийный процесс -> чаще choreography;
  • длинный, критичный и часто меняющийся business flow -> чаще orchestration.

Главная мысль: choreography дает больше автономности, orchestration дает больше управляемости и прозрачности.


Eventual Consistency И Промежуточные Состояния

Saga почти всегда означает eventual consistency.

Это значит:

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

Пример:

заказ уже создан,
но платеж еще не проведен

Это нормальное промежуточное состояние для Saga.

Поэтому в дизайне саги важно:

  • явно задавать статусы вроде PENDING, RESERVED, COMPENSATING, FAILED, COMPENSATED;
  • понимать allowed transitions;
  • не притворяться, что процесс "атомарный как SQL transaction";
  • проектировать доменную модель так, чтобы она выдерживала промежуточные состояния.

Плохая модель:

  • только CREATED и DONE

Хорошая модель:

  • PENDING
  • AWAITING_PAYMENT
  • RESERVED
  • PAYMENT_CONFIRMED
  • SHIPPING_PENDING
  • FAILED
  • COMPENSATED

Бизнес-Инварианты И Границы Гарантий Saga

До выбора шагов и компенсаций надо явно сформулировать бизнес-инварианты.

Инвариант — это не "какой у нас flow", а "что в системе не должно быть нарушено".

Примеры инвариантов для заказа:

  • нельзя иметь CONFIRMED order без успешной оплаты;
  • нельзя иметь финально списанные деньги без заказа или процесса возврата;
  • резерв товара должен в итоге перейти либо в CONFIRMED, либо в RELEASED;
  • по одному idempotency key нельзя списать деньги дважды.

Что Saga обычно обязана гарантировать:

  • система в итоге приходит в один из допустимых бизнес-статусов;
  • выполненные шаги либо доводятся до результата, либо компенсируются, либо эскалируются в manual recovery;
  • дубли команд не должны создавать дубли бизнес-эффектов, если идемпотентность реализована правильно.

Что Saga не обязана гарантировать сама по себе:

  • мгновенную глобальную консистентность;
  • глобальную serializable isolation;
  • end-to-end exactly-once бизнес-эффект;
  • автоматическое исправление необратимых внешних side effect;
  • отсутствие промежуточных состояний, видимых другим системам.

Senior-level мысль здесь такая: Saga проектируют не вокруг "набора RPC-вызовов", а вокруг явных бизнес-инвариантов и границ допустимого рассогласования.


Пользовательская Семантика И API-Статусы

Если внутри системы работает Saga, API не должно врать клиенту, будто операция уже финализирована, когда это не так.

PENDING:

  • запрос принят, но итог еще не финален;
  • клиенту лучше возвращать operation_id, saga_id или стабильный resource id;
  • клиент должен уметь poll, subscribe или получить callback позже.

FAILED:

  • с точки зрения клиента операция завершилась неуспешно;
  • если поддерживается безопасный retry, нужен idempotency key;
  • API должно ясно разделять business failure и transient infrastructure failure.

COMPENSATING:

  • система уже начала действия и сейчас их логически откатывает;
  • для клиента это не "успех" и не обычный "failed", а переходное восстановительное состояние;
  • в этом статусе особенно важно не провоцировать пользователя на повторный запрос без идемпотентности.

COMPENSATED:

  • восстановление завершено;
  • итоговая бизнес-операция считается неуспешной, но система приведена в допустимое состояние.

Практическое правило:

  • API-контракт должен описывать, какие статусы terminal, а какие нет;
  • UI и интеграции должны понимать, что PENDING и COMPENSATING — это состояния процесса, а не финальный результат;
  • пользовательская семантика должна быть согласована с внутренней моделью saga state machine.

Локальная Транзакция Внутри Шага

У Saga нет глобальной ACID-транзакции между сервисами, но внутри каждого отдельного шага обычно есть локальная атомарность.

Пример для Payment Service:

  • записать платеж в свою БД;
  • обновить статус;
  • записать событие в outbox;
  • закоммитить это одной локальной транзакцией.

То есть внутри сервиса ACID есть, но между сервисами общей транзакции нет.


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

Идемпотентность — одно из самых важных требований в Saga.

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

Примеры:

  • reserve(orderId=123) два раза не должен резервировать товар дважды;
  • refund(paymentId=777) два раза не должен возвращать деньги дважды.

Почему это критично:

  • сообщения могут дублироваться;
  • возможны таймауты;
  • retry почти неизбежен;
  • сервис может упасть после выполнения, но до подтверждения результата.

Обычно для этого используют:

  • saga_id
  • command_id
  • transaction_id
  • idempotency_key

Сервис хранит, видел ли он такую команду раньше, и на повторе не делает действие заново, а возвращает уже зафиксированный результат.

Это относится и к обычным шагам, и к компенсациям.


Retry, Timeout И Зависшие Saga

Нельзя проектировать Saga так, будто все ответы придут мгновенно и всегда.

Нужно явно решать:

  • сколько ждать следующий шаг;
  • сколько раз делать retry;
  • какие ошибки считать временными;
  • какие ошибки считать постоянными;
  • когда запускать компенсацию;
  • когда помечать сагу как FAILED;
  • кто поднимает алерт на stuck saga;
  • кто отвечает за manual recovery.

Полезно различать два типа ошибок.

Временные ошибки:

  • сеть моргнула;
  • брокер недоступен;
  • сервис перегружен;
  • временный timeout.

Их часто разумно ретраить.

Постоянные ошибки:

  • товара нет на складе;
  • карта отклонена;
  • нарушено бизнес-правило.

Тут retry обычно бессмысленен, нужна компенсация или завершение с ошибкой.

Без явной timeout policy и retry policy легко получить:

  • зависшие процессы;
  • орфанные резервы;
  • списанные деньги без финального результата;
  • ручные разборы в проде.

Outbox, Inbox И Deduplication

Saga почти никогда не живет одна. Рядом с ней обычно нужны паттерны надежной доставки сообщений.

Проблема:

  • сервис записал изменения в БД;
  • упал до отправки события;
  • другие сервисы никогда не узнали об изменении.

Для этого используют Transactional Outbox.

Идея:

  • в одной локальной транзакции сохранить бизнес-данные;
  • в той же транзакции записать событие в таблицу outbox;
  • отдельный relay/worker публикует событие в брокер и помечает его отправленным.

Для входящих сообщений часто используют:

  • inbox table
  • processed_messages
  • deduplication table

Они нужны, чтобы одно и то же сообщение не обработалось дважды.

Главная мысль: без outbox, inbox и идемпотентных обработчиков Saga быстро становится ненадежной.


Семантика Доставки Сообщений

Для Saga очень важно понимать, какую семантику доставки реально дает транспорт.

At-most-once:

  • сообщение доставляется не более одного раза;
  • дубликатов почти нет;
  • но сообщение может быть потеряно.

Это удобно по простоте, но плохо подходит для критичных бизнес-процессов.

At-least-once:

  • сообщение будет доставлено хотя бы один раз;
  • зато возможны дубли;
  • это самый частый реальный режим в distributed systems.

Поэтому в большинстве практических Saga нужно исходить именно из at-least-once и делать идемпотентность обязательной.

Exactly-once:

  • это слово часто используют маркетингово;
  • в лучшем случае оно означает ограниченную гарантию внутри конкретного брокера или pipeline;
  • оно почти никогда не означает end-to-end "ровно один бизнес-эффект" через БД, брокер и внешние API.

Главная senior-level мысль:

  • exactly-once delivery и exactly-once business effect — это не одно и то же;
  • даже если брокер дает сильные гарантии, обработчик все равно должен быть идемпотентным.

Еще один важный момент — ordering per key.

Если процесс зависит от порядка событий, обычно нужно:

  • выбрать ключ вроде orderId, paymentId, accountId;
  • направлять связанные сообщения в одну partition или sequence;
  • обрабатывать их последовательно хотя бы в рамках этого ключа.

Глобальный порядок для всей системы обычно не нужен и почти всегда слишком дорог.


Failure Matrix: Что Ломается И Что Делать

На senior-уровне Saga нужно рассматривать как систему с явной матрицей отказов.

Падает оркестратор:

  • его состояние должно быть durably сохранено;
  • после рестарта он должен поднимать незавершенные экземпляры саги;
  • если неясно, была ли команда отправлена, нужен outbox или безопасная идемпотентная пересылка;
  • таймеры, retry policy и pending commands должны восстанавливаться.

Падает участник саги:

  • у вызывающей стороны обычно возникает timeout и неопределенность результата;
  • после восстановления участник должен уметь продолжить работу из локально сохраненного состояния;
  • оркестратор или инициатор должен либо ретраить шаг, либо переводить сагу в compensation/manual flow.

Приходит дубль сообщения:

  • обработчик не должен выполнять шаг повторно;
  • нужен inbox, processed_messages или другой deduplication mechanism;
  • результат на дубль должен быть детерминированным.

Сообщения приходят не по порядку:

  • обработчик должен проверять ожидаемое состояние агрегата;
  • нельзя слепо применять событие, которое пришло раньше своего причинного предшественника;
  • где порядок критичен, нужен ordering per key.

Poison message:

  • одно и то же сообщение падает каждый раз;
  • бесконечный retry здесь вреден;
  • сообщение надо переводить в DLQ или quarantine;
  • дальше нужен алерт, диагностика и осознанное решение: исправить данные, replay, manual action.

Самый неприятный случай — unknown outcome:

  • команда могла выполниться, но подтверждение потерялось;
  • именно поэтому идемпотентность, outbox и audit trail в Saga не "желательны", а обязательны.

Состояние Saga И Данные Для Восстановления

Если подход orchestration-based, состояние обычно хранится у оркестратора. Если choreography-based, общее состояние может быть размазано, поэтому correlation_id, trace_id и saga_id становятся еще важнее.

Обычно хотят хранить:

  • saga_id
  • saga_type
  • status
  • current_step
  • payload/context
  • retry_count
  • last_error
  • created_at
  • updated_at

Часто есть и более детальная модель шагов:

saga_instance
- saga_id
- saga_type
- status
- current_step
- payload
- created_at
- updated_at

saga_step
- saga_id
- step_name
- status
- retry_count
- started_at
- completed_at
- compensation_status

Также полезен saga log, где видно:

  • какие шаги прошли;
  • какие команды были отправлены;
  • какие события были получены;
  • какие компенсации были выполнены.

Это нужно для:

  • восстановления после падения;
  • мониторинга;
  • дебага;
  • ручного восстановления.

Компенсации, Необратимые Шаги И Порядок Действий

Не у каждого шага есть идеальная компенсация.

Иногда шаг:

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

Примеры:

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

Отсюда следуют практические правила:

  • компенсацию нужно проектировать как бизнес-процесс;
  • необратимые шаги лучше ставить ближе к концу;
  • раньше лучше делать проверки и обратимые шаги;
  • порядок шагов в Saga критически важен.

Например, часто разумно:

  • сначала проверить лимиты и остатки;
  • затем сделать резерв;
  • и только потом делать необратимое списание денег.

Что Делать, Если Падает Сама Компенсация

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

На практике compensating action тоже может:

  • получить timeout;
  • упасть по инфраструктурной ошибке;
  • быть отклоненной внешней системой;
  • оказаться частично выполненной.

Partial compensation означает, что часть уже сделанных шагов была логически отменена, а часть — нет.

Пример:

  • деньги уже возвращены;
  • резерв уже снят;
  • а отмена доставки не прошла или зависла.

В такой ситуации нельзя честно говорить, что сага "полностью откатилась".

Обычно нужно:

  • хранить отдельный статус вроде COMPENSATING, COMPENSATION_FAILED, PARTIALLY_COMPENSATED;
  • ретраить компенсацию, если ошибка временная;
  • поднимать manual intervention, если автоматического завершения больше не ожидается;
  • хранить точный audit trail: что компенсировано, что нет, что ожидает оператора.

Senior-level мысль:

  • компенсация — это тоже distributed workflow;
  • для нее нужны те же требования по идемпотентности, observability и recovery, что и для forward path.

Изоляция И Конкурентные Аномалии

У Saga нет полноценной глобальной isolation, как у одной ACID-транзакции. Из-за этого возможны аномалии конкурентного доступа.

Примеры:

  • две саги одновременно пытаются купить последний товар;
  • обе видят, что товар "еще есть";
  • обе пытаются его зарезервировать.

Типичные проблемы:

  • lost update
  • чтение промежуточного состояния другой саги;
  • non-serializable behavior

Saga сама по себе не решает конкуренцию. Для этого обычно нужны:

  • локальные блокировки;
  • optimistic locking;
  • version field;
  • unique business keys;
  • reservation / hold pattern;
  • сериализация обработки по ключу;
  • partitioning by key.

Практический пример: все операции по одному orderId или accountId обрабатывать последовательно.


Semantic Locks, Reservation TTL И Hold Expiration

Saga нельзя "держать" на обычных технических блокировках уровня БД слишком долго. Для длинных процессов вместо этого часто используют semantic locks.

Semantic lock:

  • это не SELECT ... FOR UPDATE на минуты или часы;
  • это бизнес-состояние, которое временно запрещает конфликтующие действия.

Примеры:

  • заказ в статусе PENDING_PAYMENT нельзя повторно подтверждать или отменять другим flow;
  • слот или товар помечен как HELD, пока идет следующий шаг саги.

Чтобы не получить "вечные резервы", обычно добавляют:

  • reservation TTL
  • expires_at
  • hold expiration
  • фоновую очистку или release expired holds

Практическая идея:

  • если сага зависла или оркестратор умер, резерв не должен жить бесконечно;
  • срок жизни резерва должен быть частью бизнес-модели, а не случайным побочным эффектом;
  • release по TTL тоже должен быть идемпотентным и наблюдаемым.

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


Saga Как State Machine

На Saga полезно смотреть как на конечный автомат.

Состояния:

  • STARTED
  • ORDER_CREATED
  • INVENTORY_RESERVED
  • PAYMENT_DONE
  • SHIPPING_CREATED
  • COMPLETED
  • COMPENSATING
  • COMPENSATED
  • FAILED

Переходы:

  • success
  • failure
  • timeout
  • retry_exhausted

Так легче:

  • проектировать flow;
  • тестировать переходы;
  • отслеживать зависшие состояния;
  • понимать, какая компенсация допустима из какого состояния.

Версионирование Событий И Эволюция Workflow

Рано или поздно бизнес-процесс меняется:

  • добавляется новый шаг;
  • меняется порядок шагов;
  • одно событие получает новое поле;
  • старый consumer и новый orchestrator живут одновременно.

Если Saga уже есть в проде, нельзя думать о workflow как о чем-то неизменном.

Практические правила:

  • хранить у экземпляра саги workflow_version или saga_definition_version;
  • не менять смысл старого события "по месту";
  • новые поля в событиях делать по возможности additive и backward-compatible;
  • старые обработчики нельзя удалять, пока есть in-flight экземпляры старой версии.

Обычные стратегии:

  • запускать новые саги на новой версии workflow;
  • старые in-flight саги доводить по старой версии;
  • миграции делать отдельно и очень осознанно.

То же относится и к компенсациям:

  • если меняется forward flow, может измениться и compensation contract;
  • версионировать нужно не только happy path, но и recovery path.

Senior-level мысль:

  • долговечность workflow почти всегда выше, чем кажется при первом дизайне;
  • поэтому versioning должен быть встроен в модель с самого начала.

Тестовая Стратегия Для Saga

Обычных unit test на "счастливый путь" для Saga недостаточно.

Нужны как минимум несколько уровней тестирования.

State-machine tests:

  • проверяют допустимые переходы между состояниями;
  • отдельно покрывают success, failure, timeout, retry_exhausted, compensation.

Contract tests:

  • проверяют формат команд и событий между оркестратором и участниками;
  • ловят несовместимые изменения схем и ожиданий.

Replay tests:

  • позволяют воспроизвести историю саги по event log или audit log;
  • особенно полезны после продовых инцидентов.

Fault injection / chaos tests:

  • дубль сообщения;
  • reorder сообщений;
  • падение после локального commit, но до ack;
  • падение оркестратора в середине flow;
  • зависание участника и истечение TTL.

Также полезно тестировать:

  • идемпотентность обработчиков;
  • поведение DLQ;
  • ручные recovery scenario;
  • time-based logic вроде retry backoff и reservation expiration.

Если система не тестировалась на дубликаты, таймауты и unknown outcome, она не проверена по-настоящему.


Observability На Проде

В проде Saga без observability быстро превращается в "черный ящик".

Нужны метрики:

  • сколько саг стартует;
  • сколько завершается успешно;
  • сколько уходит в FAILED, COMPENSATING, COMPENSATED, NEEDS_MANUAL_REVIEW;
  • сколько retry на шаг;
  • сколько timeouts;
  • какая доля компенсаций;
  • сколько сообщений в DLQ;
  • сколько активных саг старше SLA.

Нужна trace model:

  • trace_id
  • saga_id
  • step_id
  • correlation_id
  • causation_id

По этим идентификаторам должно быть легко собрать полный путь:

  • кто инициировал сагу;
  • какая команда ушла;
  • какой шаг завершился;
  • где был retry;
  • где началась компенсация.

Нужен audit trail:

  • когда менялся статус;
  • какой payload или reference был у шага;
  • какая версия workflow использовалась;
  • кто и почему сделал manual action.

Нужны runbooks:

  • как найти stuck saga;
  • как безопасно replay или retry шаг;
  • когда допустимо вручную компенсировать;
  • когда надо эскалировать в продуктовую или операционную команду.

Senior-level мысль:

  • observability для Saga — это часть корректности системы, а не только удобство для поддержки.

Reconciliation И Manual Intervention

Даже хорошая Saga не отменяет необходимость reconciliation.

Reconciliation — это периодическая проверка, которая сравнивает реальное состояние между системами, находит расхождения и помогает их исправлять.

Примеры:

  • у нас заказ помечен как PAID, а у платежного провайдера платеж FAILED;
  • или наоборот, внешний провайдер считает оплату успешной, а наш сервис застрял в PENDING.

Также не всегда возможна полностью автоматическая компенсация. Тогда нужен manual intervention.

Обычно это означает:

  • пометить сагу как NEEDS_MANUAL_REVIEW;
  • поднять алерт или создать тикет;
  • показать оператору контекст;
  • дать возможность вручную повторить шаг, компенсировать его или завершить процесс.

В серьезных системах это не опция, а часть эксплуатационного дизайна.


Saga, TCC, Process Manager И Workflow Engines

На senior-уровне важно не только знать Saga, но и понимать, когда вместо нее лучше подходит другой подход.

Saga:

  • хорошо подходит для долгих бизнес-процессов с локальными транзакциями и компенсациями;
  • основной акцент — eventual consistency и recovery через compensating actions.

TCC:

  • требует явных операций Try, Confirm, Cancel;
  • хорошо подходит там, где ресурсы можно сначала зарезервировать, а потом либо подтвердить, либо освободить;
  • обычно дает более строгую дисциплину вокруг reservation lifecycle;
  • более инвазивен к API участников, потому что каждый участник должен явно поддерживать TCC-модель.

Process manager:

  • это более широкий паттерн координации бизнес-процесса;
  • не каждый process manager — Saga;
  • Saga orchestrator можно считать частным случаем process manager, где явно есть compensations и восстановительный flow.

Workflow engine вроде Temporal, Camunda, Conductor:

  • дает durable execution;
  • обычно уже умеет таймеры, retry, visibility, state persistence;
  • сильно упрощает длинные процессы и operational tooling;
  • но добавляет platform dependency, инфраструктурную сложность и ограничения модели исполнения.

Практическая эвристика:

  • если у участников есть хорошие reservation API -> стоит подумать о TCC;
  • если процесс длинный, меняющийся и критичный -> часто выгоден workflow engine;
  • если orchestration относительно простой -> может хватить собственного saga orchestrator;
  • если координация вообще не требует compensation semantics -> возможно нужен не Saga, а просто process manager или integration workflow.

When Not To Use Saga

Saga — не универсальное решение.

Ее лучше не использовать, если:

  • все данные и логика живут в одной БД и достаточно обычной локальной транзакции;
  • требуется строгая глобальная атомарность, а компенсация юридически или финансово неприемлема;
  • шаги почти не имеют обратимых действий;
  • процесс короткий, синхронный и не оправдывает сложность distributed workflow;
  • проблема на самом деле решается правильной границей сервисов, а не сложной координацией поверх них;
  • стоимость observability, recovery и operational support выше, чем ценность от декомпозиции.

Иногда лучший ответ — не "добавить Saga", а:

  • объединить шаги в один сервис;
  • переделать ownership данных;
  • использовать обычную транзакцию;
  • использовать TCC или workflow engine;
  • пересмотреть сам бизнес-процесс.

Senior-level решение здесь не в том, чтобы "везде применять Saga", а в том, чтобы выбирать самый дешевый по сложности механизм, который реально держит нужные инварианты.


Что Почти Всегда Нужно Рядом С Saga

Рядом с Saga обычно нужны:

  • Outbox
  • idempotent handlers
  • Inbox / processed_messages
  • retry
  • timeout policy
  • dead letter handling
  • correlation_id
  • observability
  • reconciliation
  • manual recovery

То есть Saga — это не только модель координации, но и набор сопутствующих reliability-паттернов.


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

  • Нет идемпотентности у шагов и компенсаций.
  • Нет явных статусов процесса.
  • Нет timeout handling.
  • Нет понимания, что делать со stuck saga.
  • Choreography используется там, где процесс уже слишком сложный.
  • Компенсация проектируется по принципу "потом разберемся".
  • Сервис публикует событие ненадежно без Outbox.
  • Команда обещает пользователю мгновенную глобальную консистентность там, где ее нет.
  • Игнорируются проблемы конкуренции и isolation anomalies.
  • Нет reconciliation и операционного сценария для ручного вмешательства.