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 inventorycharge payment->refund paymentcreate shipment->cancel shipment
Почему Не 2PC / XA¶
Есть два типовых подхода к распределенной бизнес-операции.
2PC / XA:
- дают строгую атомарность;
- сложны во внедрении;
- плохо ложатся на микросервисы;
- держат блокировки дольше;
- усиливают связанность;
- создают проблемы для доступности и производительности;
- не везде нормально поддерживаются брокерами и интеграциями.
Saga:
- лучше подходит для микросервисов;
- позволяет сервисам оставаться более независимыми;
- хорошо работает с асинхронностью;
- лучше масштабируется;
- требует явной логики компенсаций;
- не дает мгновенной глобальной консистентности;
- сложнее в observability и отладке.
Идея Saga не в том, чтобы "сымитировать XA под другим именем", а в том, чтобы принять другую модель согласованности и управления ошибками.
Из Чего Состоит Saga¶
В нормальном дизайне у Saga есть несколько обязательных частей.
Шаги:
- каждый шаг — отдельная локальная транзакция;
- шаг должен быть атомарным в пределах своего сервиса.
Компенсации:
- для обратимых шагов обычно есть compensating action;
- компенсация тоже должна проектироваться как отдельное бизнес-действие.
Контекст Saga:
saga_idstatuscurrent_steppayload/contextcorrelation_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)
Каждый шаг:
- делает локальную транзакцию;
- коммитит ее;
- инициирует следующий шаг.
Сценарий с ошибкой:
То есть откат идет не через глобальный 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
Хорошая модель:
PENDINGAWAITING_PAYMENTRESERVEDPAYMENT_CONFIRMEDSHIPPING_PENDINGFAILEDCOMPENSATED
Бизнес-Инварианты И Границы Гарантий 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_idcommand_idtransaction_ididempotency_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 tableprocessed_messagesdeduplication 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_idsaga_typestatuscurrent_steppayload/contextretry_countlast_errorcreated_atupdated_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 TTLexpires_athold expiration- фоновую очистку или release expired holds
Практическая идея:
- если сага зависла или оркестратор умер, резерв не должен жить бесконечно;
- срок жизни резерва должен быть частью бизнес-модели, а не случайным побочным эффектом;
- release по TTL тоже должен быть идемпотентным и наблюдаемым.
Такой дизайн часто важнее, чем попытка силой навязать глобальную изоляцию через долгие блокировки.
Saga Как State Machine¶
На Saga полезно смотреть как на конечный автомат.
Состояния:
STARTEDORDER_CREATEDINVENTORY_RESERVEDPAYMENT_DONESHIPPING_CREATEDCOMPLETEDCOMPENSATINGCOMPENSATEDFAILED
Переходы:
successfailuretimeoutretry_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_idsaga_idstep_idcorrelation_idcausation_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 обычно нужны:
Outboxidempotent handlersInbox / processed_messagesretrytimeout policydead letter handlingcorrelation_idobservabilityreconciliationmanual recovery
То есть Saga — это не только модель координации, но и набор сопутствующих reliability-паттернов.
Типичные Ошибки¶
- Нет идемпотентности у шагов и компенсаций.
- Нет явных статусов процесса.
- Нет
timeout handling. - Нет понимания, что делать со stuck saga.
Choreographyиспользуется там, где процесс уже слишком сложный.- Компенсация проектируется по принципу "потом разберемся".
- Сервис публикует событие ненадежно без
Outbox. - Команда обещает пользователю мгновенную глобальную консистентность там, где ее нет.
- Игнорируются проблемы конкуренции и
isolation anomalies. - Нет
reconciliationи операционного сценария для ручного вмешательства.