Оглавление
- Введение
- Принципы работы паттерна Saga
- Сравнение подходов Saga: хореография vs оркестрация
- Архитектура платежной системы с паттерном Saga
- Диаграммы последовательности Saga
- Пример реализации Saga на Java (Spring Boot)
- Обработка ошибок и отказов в Saga
- Сравнительная таблица
- Заключение
Введение
В эпоху микросервисной архитектуры разработчики сталкиваются с проблемой реализации транзакций, охватывающих несколько распределенных сервисов. Классические ACID-транзакции, применяемые в монолитных системах, оказываются неприменимыми в среде распределенных приложений, так как каждый сервис управляет собственной базой данных и не поддерживает общие механизмы транзакционности. При этом традиционный подход с двухфазным коммитом (2PC) часто не подходит из-за его блокирующего характера, снижающего производительность и масштабируемость системы.
Именно здесь на помощь приходит паттерн Saga – архитектурный подход к реализации длительных бизнес-транзакций, которые охватывают несколько сервисов и требуют поддержки eventual consistency (конечной согласованности). Вместо единой глобальной транзакции Saga разделяет бизнес-операцию на последовательность независимых локальных транзакций, каждая из которых выполняется в отдельном микросервисе. В случае возникновения сбоя или ошибки в одном из шагов Saga компенсирует уже выполненные транзакции специальными компенсирующими действиями (compensating transactions), логически возвращающими систему в предыдущее согласованное состояние.
Существует два основных способа организации Saga: оркестрация (Orchestration) и хореография (Choreography). При оркестрации управление последовательностью шагов и компенсаций берет на себя отдельный компонент – оркестратор. Он последовательно вызывает сервисы, получает от них подтверждения и в случае необходимости запускает компенсирующие действия (“откаты”). При хореографии же каждый сервис действует автономно, реагируя на события и публикуя новые события в общий брокер. Такой подход обеспечивает максимальную гибкость и слабую связанность сервисов, но усложняет мониторинг и отладку бизнес-процессов.
Понимание различий между оркестрацией и хореографией Saga, их преимуществ и недостатков, а также умение правильно выбрать подход, исходя из требований конкретного бизнес-кейса, становится критическим навыком для разработчиков и архитекторов распределенных систем. В этой статье мы детально разберем оба подхода на примере учебной платежной системы, дополним объяснения понятными диаграммами, а также увидим практические примеры реализации Saga на Java с использованием Spring Boot и REST API.
Принципы работы паттерна Saga
Локальные и компенсирующие транзакции. Каждая Saga представляет собой последовательность локальных транзакций. Локальная транзакция – это операция внутри одного сервиса, выполняющаяся атомарно (с обычной ACID-транзакцией в его базе данных). После успешного выполнения она может опубликовать событие или отправить сообщение, чтобы инициировать следующую операцию Saga. В случае ошибки в любом шаге Saga уже выполненные действия компенсируются компенсирующими транзакциями – логическими операциями, возвращающими данные в прежнее состояние (например, отмена платежа, возврат резервирования и т.п.). Разработчик должен явно предусмотреть такие компенсации – автоматического rollback, как в монолитной транзакции, здесь нет. При проектировании Saga важно определить точку невозврата – после какого шага отмена предыдущих уже невозможна (так называемая pivot-транзакция). До наступления pivot-шагов все действия должны быть обратимы, а после – оставшиеся шаги должны гарантированно доводиться до конца, возможно, с ретраями, чтобы система дошла до консистентного состояния.
Хореография Saga (обмен событиями)
При хореографии нет единого управляющего компонента; вместо этого сервисы обмениваются событиями напрямую. Каждая локальная транзакция публикует доменное событие, которое подхватывается другими сервисами и запускает у них соответствующие операции Saga. Такая архитектура весьма гибкая: каждый микросервис знает только о собственных действиях и подписывается на события, не зная, кто их изначально сгенерировал. В контексте учебной платежной системы это означает, например, что сервис заказа после создания заказа публикует событие OrderCreated
, на которое реагирует платежный сервис, выполняющий списание средств, а сервис уведомлений может реагировать своим действием и т.д., без явных вызовов друг друга.
Пример хореографии Saga: допустим, клиент инициирует платеж, связанный с созданием нового заказа. Последовательность шагов будет следующей:
- Order Service (сервис заказов) получает запрос на создание нового заказа/платежа. Он создает заказ в статусе Pending (ожидается) в своей базе данных и публикует событие
OrderCreated
(например, в шину сообщений или брокер событий). - Payment Service (платежный сервис) получает событие
OrderCreated
и инициирует локальную транзакцию резервирования средств клиента. Например, он пытается заблокировать нужную сумму на счете или кредитной линии. Результат операции он оформляет как новое событие: либоPaymentReserved
(оплата зарезервирована), либоPaymentFailed
(ошибка платежа, например недостаточно средств). - Order Service получает событие результата платежа. Если пришло
PaymentReserved
(и, при необходимости, другие сервисы подтвердили свои шаги), заказ помечается как Approved (подтвержден, оплачен) и обновляется в базе данных. Если же получено событиеPaymentFailed
, сервис заказа помечает заказ как Cancelled (отменен) и выполняет необходимые компенсирующие действия – например, освобождает зарезервированные ресурсы, уведомляет пользователя об ошибке. В данном простом случае дополнительных компенсирующих операций может не потребоваться, так как при неудаче платежа заказ просто отменяется и деньги не списаны.
Хореографическая Saga иллюстрирует асинхронный, событийный поток управления. Каждый сервис исполняет свой шаг и “передает эстафету” дальше через событие. При этом участники саги слабо связаны между собой, что упрощает эволюцию системы – можно добавлять новые шаги, подписывая новые сервисы на события, без изменения существующего кода. Однако сложность может возрасти: с увеличением числа участников проследить весь бизнес-процесс становится трудно, логика распределена по разным сервисам, а последовательность событий не сразу очевидна. Отладка также затрудняется – чтобы проследить поток, нужно собрать логи многих сервисов. Кроме того, возникает риск запутанных циклов: сервисы могут по кругу реагировать на события друг друга, если не продумать тщательно схему взаимодействия.
Когда применяется хореография: этот подход хорошо работает для относительно простых процессов с небольшим числом шагов, где нет жестких требований к порядку выполнения. Он естественно подходит, когда система развивалась из монолита – можно постепенно выделять события и реакции на них вместо централизации логики. Хореография также распространена в средах event-driven архитектуры и serverless, где компоненты живут кратко и реагируют на события. В распределенной платежной системе хореография удобна, когда сервисы максимально автономны (например, сервис оплаты, сервис выставления счетов и сервис уведомлений могут работать через события, не зная о существовании друг друга). При этом важно, чтобы порядок событий не был критичным; иначе придется тщательно документировать и согласовывать последовательность действий.
Оркестрация Saga (централизованный координатор)
В подходе оркестрации вводится специальный компонент – Saga Orchestrator (оркестратор саги), который управляет всем процессом. Он знает последовательность шагов бизнес-процесса и отдает команды участникам выполнить ту или иную операцию, а также отслеживает результаты и при необходимости запускает компенсации. Оркестратор можно представить как диспетчера: он «дирижирует» микросервисами, вызывая их в нужном порядке и обрабатывая успехи/ошибки каждого шага. Оркестратор может быть реализован как отдельный сервис или как модуль внутри какого-то сервиса (например, внутри Order Service для сценария создания заказа).
Пример оркестрации Saga: снова рассмотрим процесс оплаты заказа. В этом случае Order Service сам координирует сагу (через встроенный компонент-оркестратор):
- Order Service получает запрос от пользователя на новый заказ/платеж. Он создает запись заказа в статусе Pending в своей базе данных, после чего создается объект Saga-оркестратора для этого заказа (например, экземпляр процесса с уникальным ID).
- Saga Orchestrator (внутри Order Service или в отдельном сервисе) берет на себя дальнейшие шаги. Он отправляет команду ReservePayment (зарезервировать средства) в Payment Service – например, выполняет REST-вызов или отправляет сообщение с запросом на списание/блокировку суммы.
- Payment Service выполняет локальную транзакцию резервирования денег. Затем он возвращает результат оркестратору – либо успешное подтверждение, либо информацию об ошибке (например, недостаточно средств). В случае асинхронного взаимодействия это может быть ответное сообщение/событие, в случае синхронного REST – код ответа/данные.
- Saga Orchestrator получает результат от Payment Service. Если платеж успешно заблокирован, оркестратор отправляет следующую команду следующему участнику саги. Например, можно зарезервировать товар на складе: команда ReserveInventory уходит в Inventory Service. После успешного резервирования товара – следующая команда дальше, и так до финального шага.
- Когда все необходимые сервисы выполнили свои локальные транзакции, оркестратор отправляет команду ApproveOrder в Order Service, и заказ помечается как Approved (завершен). Если на каком-то этапе пришел отрицательный ответ (ошибка), оркестратор инициирует цепочку компенсационных команд: уже выполненным сервисам посылаются команды отмены их действий в обратном порядке. Например, если резервирование товара не удалось, оркестратор пошлет Payment Service команду RefundPayment (разблокировать ранее удержанные средства), а Order Service получит команду RejectOrder для отмены заказа.
- Order Service в финале получает команду от оркестратора либо подтвердить заказ (статус Approved, если все шаги прошли), либо отменить его (статус Cancelled, если произошел сбой и выполнены откаты). Клиенту возвращается результат – либо успешное подтверждение платежа, либо уведомление об неудаче (с возможностью повторить попытку или сообщением об отказе).
Оркестрация дает централизованный контроль над потоком саги. Все решения о том, кто и что делает дальше, принимает оркестратор, что упрощает понимание процесса. В логах оркестратора видна полная картина, и отладка поэтому проще, чем при разрозненных событиях. Код микросервисов-участников остается более простым: каждый выполняет команду по запросу, не нужно знать о всей саге целиком. Кроме того, избегаются циклические зависимости – сервисы не вызывают друг друга напрямую, они общаются только с оркестратором, что схоже с ESB (Enterprise Service Bus).
Когда применяется оркестрация: этот подход оправдан для сложных многошаговых процессов или когда необходим строгий контроль порядка выполнения. Если в платежной системе нужно гарантированно выполнять шаги последовательно (например, сначала оплатить, потом отгрузить товар, затем уведомить системы бухгалтерии и т.п.), оркестратор обеспечит нужную упорядоченность. Оркестрация часто используется с новыми проектами (greenfield), где с самого начала можно заложить центральный сервис управления процессами. Также она хорошо сочетается с API Gateway – фронт может дернуть один API, а оркестратор за ним сделает серию вызовов нескольких сервисов. Недостаток – сам оркестратор становится критичным компонентом: в нем сконцентрирована логика, и в случае его отказа или ошибок страдает весь процесс. Нужно продумывать отказоустойчивость оркестратора (репликацию, восстановление). Кроме того, чрезмерная централизация может снизить гибкость – при добавлении новых шагов приходится менять код оркестратора. Тем не менее, благодаря явной координации, оркестрация часто предпочтительна для финансовых транзакций, где порядок и учет ошибок критически важны.
Сравнение подходов Saga: хореография vs оркестрация
Оба подхода достигают одной цели – распределенной транзакционности – но с разными достоинствами и компромиссами.
- Простота vs. контроль. Хореография проще в начальной реализации: достаточно подписать сервисы на события друг друга, дополнительный координатор не нужен. Это ускоряет разработку простых сценариев и снижает сопряженность между сервисами – каждый сервис автономен и лишь публикует/слушает события. Оркестрация же требует написать отдельный сервис (или модуль) с логикой координации, что увеличивает сложность дизайна. Зато оркестратор обеспечивает явный глобальный контроль порядка шагов. Для бизнес-процессов с жесткой последовательностью это большой плюс: легче проследить и изменить поток операций в одном месте, чем отлавливать его в событиях по всему коду.
- Связность и расширяемость. При хореографии новые шаги добавляются относительно легко – достаточно опубликовать/подписаться на нужные события, не меняя существующие сервисы. Архитектура получается расширяемой и гибкой. Однако с ростом системы понимание потока резко усложняется: чтобы добавить шаг в середину бизнес-процесса, надо убедиться, что все события будут обработаны в верном порядке, что не возникнет гонок и т.д. Оркестратор, напротив, является центральной точкой изменений: новый шаг просто добавляется в его логику. Минус – сервисы-участники при оркестрации зависят от команд оркестратора (т.е. знают, что кто-то ими управляет), но сами оркестратор и участники слабо связаны между собой (обычно через четко определенные интерфейсы). Циклические зависимости между сервисами при оркестрации исключены – каждый знает только об оркестраторе, а не друг о друге.
- Отказоустойчивость. Хореография распределяет ответственность между сервисами, поэтому отсутствует единая точка отказа – сбой одного сервиса затрагивает только связанные с ним операции Saga. Оркестратор же сам по себе становится единым узлом, отказ которого нарушит выполнение всех саг, им управляемых. Требуется внедрять механизмы резервирования и масштабирования оркестратора, чтобы устранить бутылочное горлышко. Интересно, что современные фреймворки (например, Temporal, Camunda, AWS Step Functions) реализуют надежный оркестратор Saga без единой точки отказа, журналируя прогресс саги и перезапуская ее при сбое с того же шага. Таким образом, можно получить преимущества оркестрации без снижения надежности – ценой использования специализированного инструмента.
- Отладка и мониторинг. Централизованная Saga легче поддается трассировке – можно логировать шаги в оркестраторе и иметь полную картину. В хореографии придется коррелировать события из логов разных сервисов, что требует встроенного correlation-id и распределенного трейсинга. Интеграционное тестирование саги при хореографии тоже сложнее: для воспроизведения сценария нужно поднять все сервисы и брокер событий. В оркестрации можно тестировать логику потока отдельно, замокав вызовы сервисов. С другой стороны, при хореографии каждая служба проще и ее можно тестировать изолированно (реагирует на событие -> делает работу -> публикует результат).
Итог: для простых и эволюционирующих процессов хореография зачастую естественнее и проще, но для сложных, строго упорядоченных транзакций с нетривиальной логикой лучше подходит оркестратор. На практике нередко используют гибридные подходы – например, оркестратор управляет крупными этапами, а внутри какого-то шага сервисы могут обменяться несколькими событиями самостоятельно. В распределенных платежах, где цена ошибки высока, чаще склоняются к оркестрации либо используют надежные фреймворки, обеспечивающие гарантированное выполнение саг (та же Temporal, встроенные механизмы Saga в Spring Cloud, Axon Framework и пр.).
Cравнительная таблица
Характеристика | Оркестрация | Хореография |
---|---|---|
Наличие централизованного координатора | Есть (оркестратор управляет процессом) | Нет (обмен событиями между сервисами) |
Связность сервисов | Средняя (сервис зависит от оркестратора) | Низкая (сервисы не знают друг о друге) |
Простота управления процессом | Высокая (легко отслеживать и менять последовательность) | Средняя/Низкая (сложно проследить процесс в целом) |
Гибкость изменений | Средняя/Низкая (нужно менять оркестратор при изменениях) | Высокая (легко добавлять новые шаги через события) |
Отказоустойчивость | Средняя (есть риск отказа оркестратора – единой точки отказа) | Высокая (нет единой точки отказа) |
Легкость отладки | Высокая (все логирование в оркестраторе) | Низкая (необходима корреляция логов из разных сервисов) |
Поддержка сложных процессов | Легко (идеально для сложных, упорядоченных сценариев) | Сложнее (подходит для простых или умеренно сложных сценариев) |
Риск циклических зависимостей | Отсутствует (все связи через оркестратор) | Возможен (сервисы могут вызвать друг друга по кругу) |
Скорость первоначальной реализации | Средняя (нужно написать отдельный оркестратор) | Высокая (быстро развернуть события и слушателей) |
Масштабируемость | Средняя (нужно масштабировать оркестратор) | Высокая (легко масштабировать отдельные сервисы) |
Обработка ошибок и компенсация | Явная (оркестратор явно контролирует компенсации) | Неявная (компенсация через события – сложнее контролировать) |
Когда использовать:
- Оркестрация:
Для сложных, упорядоченных, финансово-критичных сценариев, где важна прозрачность процесса и простая отладка. - Хореография:
Для простых, слабо связанных, быстро изменяющихся процессов, где важна максимальная гибкость и децентрализация.
Архитектура платежной системы с паттерном Saga

Рис. 1: Архитектура микросервисной платежной системы с Saga (оркестрация).
Диаграмма демонстрирует основные компоненты. Пользователь инициирует платеж через API Gateway или напрямую вызывая Order Service. Order Service содержит логику Saga (оркестратор) и взаимодействует с другими сервисами: Payment Service для списания средств и Inventory Service для резервирования товара (например, при оплате заказа). Каждый сервис имеет собственную базу данных (DB), соблюдая принцип «Database per service». Оркестратор координирует последовательность: сначала вызывает Payment Service (резервирует платеж), затем Inventory Service (резервирует товар), и в конце уведомляет Order Service о подтверждении или отмене заказа. Все локальные операции выполняются в рамках транзакций в их БД. Коммуникация между сервисами осуществляется через синхронные REST-вызовы или асинхронно через брокер сообщений.
Как показано на рис. 1, Saga оркестратор может быть реализован внутри сервиса заказов. В примере, когда пользователь создает новый заказ с оплатой, Order Service сначала сохраняет заказ (статус «Pending») в своей базе. Затем оркестратор выполняет вызов в Payment Service для резервирования средств. Если успешно, затем вызывается Inventory Service для резервирования товара. После всех успешных шагов Order Service обновляет статус заказа на «Approved». Если на каком-то этапе происходит ошибка, оркестратор инициирует откат: ранее зарезервированные средства освобождаются (команда Payment Service на отмену платежа), а заказ помечается «Cancelled». Все взаимодействия между сервисами можно реализовать через REST API (синхронно) или через обмен сообщениями (асинхронно) – выбор зависит от требований к задержкам и сложности реализации.
Отметим, что в варианте хореографии подобная архитектура не имеет явного оркестратора. Вместо этого сервисы обмениваются событиями через брокер. Например, Order Service после создания заказа отправит событие в Message Broker, Payment Service, получив событие, выполнит локальную транзакцию платежа и отправит новое событие результата, которое обработает Order Service и Inventory Service. То есть, на диаграмме оркестратор отсутствует, а между сервисами расположен брокер событий, через который идет коммуникация. Сами же контейнеры (Order, Payment, Inventory Service и их базы) сохраняются, просто меняется способ их связи. В реальных платежных системах часто используется брокер (Kafka, RabbitMQ и др.) для реализации Saga-хореографии, особенно когда требуется масштабировать обработку событий или интегрироваться с внешними асинхронными системами (например, с внешними платежными шлюзами).
Диаграммы последовательности Saga
Ниже приведены последовательности шагов для саги обоих типов координации применительно к сценарию оплаты заказа.
Saga с оркестрацией – последовательность операций

Рис. 2: Последовательность выполнения Saga (оркестрация).
Диаграмма иллюстрирует поток сообщений между участниками при оркестрации. Оркестратор (в составе Order Service) последовательно вызывает Payment Service и другие сервисы, получая от них ответы. В примере после создания заказа (Order Pending
) оркестратор отправляет команду резервирования средств (Reserve Credit
) в Payment Service. Payment Service выполняет локальную транзакцию (резервирует деньги) и возвращает ответ (Credit Reserved
или отказ). Оркестратор на основе ответа либо продолжает сагу (например, отправляет команду следующему сервису – здесь опущено на диаграмме – или сразу подтверждает заказ), либо инициирует откат. В случае успешного платежа оркестратор отправляет команду подтверждения заказа (Approve Order
), и Order Service завершает транзакцию (статус Approved). В случае ошибки платежа оркестратор отправляет команду отмены заказа (Reject Order
), и Order Service откатывает изменения (статус Cancelled).
На рисунке 2 показан пример с двумя шагами (платеж и подтверждение заказа). В реальности Saga-оркестратор мог бы включать и больше операций – например, после резервирования платежа вызывать сервис склада для проверки наличия товара, сервис доставки для расчета отправки и т.д. Оркестратор управляет этими вызовами последовательно, ожидая подтверждения каждого шага. Если какой-то шаг не удался, он запускает компенсирующие команды для уже выполненных. Например, если резервирование товара не удалось после успешного платежа, то оркестратор отменит платеж (вернет деньги) и только потом отменит заказ – это гарантирует, что система не останется в нек konsистентном состоянии (денег не снято, товар не зарезервирован, заказ отменен). Временные задержки между шагами можно обрабатывать с помощью ожиданий или асинхронных ответов. Оркестратор, как правило, ведет состояние саги (статус каждого шага), что помогает в случае сбоя рестартовать процесс с нужного места.
Saga с хореографией – последовательность операций

Рис. 3: Последовательность выполнения Saga (хореография).
Диаграмма демонстрирует обмен событиями между сервисами без центрального координатора. После создания заказа сервис Order публикует событие OrderCreated
в общий канал (шину). Payment Service, получив это событие, выполняет локальный платеж (резервирует средства). В случае успеха он публикует событие PaymentApproved
, в случае ошибки – PaymentFailed
. Order Service подписан на эти события и в ответ либо подтверждает заказ (изменяет статус на Approved), либо отменяет его (Cancelled). На диаграмме также показано участие Customer Service, выступающего в роли платежного сервиса (резервирование кредитного лимита клиента). Каждый сервис работает независимо: обрабатывает событие и при необходимости генерирует новое. Поток управления “выявляется” из последовательности событий, проходящих через брокер.
Как видно из рис. 3, события являются триггерами шагов Saga. В нашем платежном примере первый шаг – событие OrderCreated
– запускает сразу несколько потенциальных действий: Payment Service бронирует оплату, Inventory Service мог бы параллельно забронировать товар (если бизнес-логика допускает параллельность). Они работают независимо друг от друга. Порядок завершения этих шагов не гарантирован, поэтому иногда добавляют координирующие события. Например, Order Service может ожидать двух событий – об успешном платеже и успешном резерве товара – прежде чем менять статус на Approved. Если же приходит событие об ошибке от любого участника (PaymentFailed
или гипотетическое InventoryFailed
), Order Service сразу отменит заказ и может разослать события компенсации остальным (хотя часто достаточно сам факт отмены заказа – другие сервисы при получении такого события сами откатят свои изменения).
В реализации хореографии ключевую роль играет надежность доставки событий. Все события обычно проходят через брокер сообщений (Kafka, RabbitMQ и др.), поддерживающий хранение и повторную доставку. Сервисы должны быть способны идемпотентно обрабатывать дубли событий, а также хранить у себя состояние, чтобы при перезапуске возобновить обработку. В примере, если Order Service перезапустится, он может заново прочитать события PaymentApproved/Failed
из очереди, сопоставить с текущим статусом заказа и принять решение. Это добавляет сложности, но избавляет от единой точки отказа. В современных платежных системах часто используется Kafka-событийная хореография: например, заказ, платеж, инвентарь и уведомления общаются через события, а система в целом достигает в итоге согласованного состояния.
Пример реализации Saga на Java (Spring Boot)
Рассмотрим упрощенную реализацию Saga-паттерна на языке Java с использованием Spring Boot. Предположим, у нас есть два микросервиса: OrderService (обрабатывает заказы) и PaymentService (обрабатывает платежи). Требуется обеспечить единую бизнес-транзакцию на оба сервиса: создание заказа должно сопровождаться оплатой, и либо оба шага успешны, либо при неуспехе платежа заказ должен автоматически отменяться. Покажем ключевые фрагменты кода для обоих подходов – оркестрационного и хореографического.
Оркестрация Saga: Spring Boot + REST
В оркестрационном варианте можно вынести логику координации в отдельный компонент, например, класс OrderSagaOrchestrator, который будет вызываться из OrderService. Он последовательно вызывает внешние сервисы и обрабатывает результаты. Для упрощения возьмем синхронные REST-вызовы (например, с помощью RestTemplate
или Feign-клиента) – в реальном мире можно использовать и асинхронный обмен сообщениями, принцип схож.
@Service
public class OrderSagaOrchestrator {
@Autowired
private OrderRepository orderRepo;
@Autowired
private PaymentClient paymentClient; // Feign or RestTemplate wrapper
@Transactional
public Order processOrder(Order order) {
// 1. Локальная транзакция сервиса заказа
order.setStatus(Status.PENDING);
orderRepo.save(order);
try {
// 2. Шаг Saga: резервирование платежа через Payment Service
paymentClient.reservePayment(order.getId(), order.getTotal());
// 3. Можно добавить другие шаги Saga, например, вызов InventoryService
// inventoryClient.reserveItems(order.getId(), order.getItems());
// 4. Финальный шаг Saga: подтверждение заказа
order.setStatus(Status.APPROVED);
orderRepo.save(order);
} catch (Exception e) {
// Если на любом шаге произошла ошибка, выполняем компенсирующие действия:
// Откат платежа (команда refund во внешний Payment Service)
try {
paymentClient.cancelPayment(order.getId());
} catch (Exception compEx) {
// Логируем, при невозможности отката – эскалация (manual alert)
}
// Откат локальной транзакции заказа
order.setStatus(Status.CANCELLED);
orderRepo.save(order);
}
return order;
}
}
В этом коде метод processOrder
проводит заказ через сагу. Сначала записывается новый заказ со статусом «Pending» в базе OrderService. Затем в блоке try
вызывается внешний метод paymentClient.reservePayment(...)
– это REST-вызов в PaymentService, который пытается списать или зарезервировать средства. Если он прошел (исключение не возникло), мы могли бы вызвать следующий сервис (закомментировано – InventoryService, при наличии). После успешных шагов меняем статус заказа на «Approved». Если же вызов PaymentService выбросил исключение (например, REST-клиент получил 400/500 или таймаут), управление переходит в блок catch
. Здесь мы вызываем компенсацию: paymentClient.cancelPayment(...)
– метод в PaymentService, который должен отменить ранее резервированные средства (например, разблокировать деньги, если они успели заблокироваться). Далее меняем статус заказа на «Cancelled». Мы обернули отмену платежа в отдельный try
, так как и она может теоретически не выполниться (скажем, PaymentService недоступен). В таком случае ситуация сложная – заказ уже помечаем отмененным, но деньги могут остаться заблокированными; в реальных системах такие случаи требуют либо ретраев, либо ручного разбора (например, финансовый отдел позже проверит и исправит). В нашем примере мы просто логируем ошибку компенсации.
Обратите внимание, что методы orderRepo.save
и вызовы внешних сервисов помечены аннотацией @Transactional
. Локальная транзакция в OrderService гарантирует, что запись заказа будет зафиксирована в БД до вызова внешнего сервиса. Однако тут возникает тонкий момент: что если OrderService упадет после сохранения заказа, но до вызова Payment? Заказ останется в Pending без платежа. Для решения обычно применяют шаблон Transactional Outbox – вместо сразу вызывать PaymentService, мы записываем событие-задачу в специальную таблицу outbox в той же транзакции, и отдельный компонент читает из нее и вызывает PaymentService. Это предотвращает проблему разрыва между локальной транзакцией и внешним вызовом. В нашем упрощенном коде мы этого не делаем, но в боевых системах так достигается атомарность: либо заказ и «задание на платеж» записаны, либо ничего (если транзакция откатилась).
В самом PaymentService с оркестратором код довольно простой: он предоставляет методы, которые вызываются оркестратором. Например, псевдокод PaymentService:
@RestController
public class PaymentController {
@Autowired
PaymentRepository payRepo;
@PostMapping("/payments/{orderId}/reserve")
public ResponseEntity<?> reservePayment(@PathVariable Long orderId, @RequestBody BigDecimal amount) {
// зарезервировать средства (списать с баланса или отметить долг)
boolean success = paymentService.reserve(orderId, amount);
if (!success) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body("Insufficient funds");
}
return ResponseEntity.ok().build();
}
@PostMapping("/payments/{orderId}/cancel")
public ResponseEntity<?> cancelPayment(@PathVariable Long orderId) {
paymentService.rollback(orderId);
return ResponseEntity.ok().build();
}
}
Методы reserve
и rollback
внутри paymentService
(бизнес-логика) работают в локальной транзакции PaymentService: первый, например, сохраняет запись платежа со статусом “Reserved” или уменьшает баланс счета; второй либо удаляет эту запись, либо помечает ее отмененной, возвращая деньги. Важно, что оба внешних вызова (reservePayment
и cancelPayment
) идемпотентны – их можно звать повторно без негативных последствий. Это нужно, чтобы оркестратор мог безопасно ретраить вызовы в случае временных сбоев сети, не рискуя задублировать списание денег.
Стоит отметить, что вместо самостоятельного написания кода оркестрации, можно воспользоваться готовыми инструментами. Существуют Spring-модули и внешние библиотеки: например, Spring State Machine, Camunda, Axon Framework, Eventuate Tram (от Криса Ричардсона) и др., которые предоставляют абстракции Saga. Они могут сохранять состояние саги, автоматически вызывать компенсации при исключениях и т.п. В простых случаях, однако, разработчики нередко реализуют сагу вручную, как в нашем примере, используя знакомые средства (сервисы, RestTemplate и try/catch).
Хореография Saga: Spring Boot + события (Kafka)
Теперь рассмотрим реализацию того же бизнес-процесса средствами хореографии. Здесь каждый микросервис будет самостоятельно публиковать события и реагировать на них. В экосистеме Spring Boot для организации такого взаимодействия часто используют Spring Cloud Stream или простой JmsTemplate
/Kafka API. В примере покажем упрощенно с использованием Spring Events (ApplicationEventPublisher) для наглядности, хотя в распределенной системе лучше использовать брокер сообщений.
OrderService – после создания заказа генерирует событие OrderCreated
:
@Service
public class OrderService {
@Autowired
OrderRepository orderRepo;
@Autowired
ApplicationEventPublisher publisher; // для публикации событий
@Transactional
public Order createOrder(Order order) {
order.setStatus(Status.PENDING);
orderRepo.save(order);
// публикуем событие о создании заказа
OrderCreatedEvent event = new OrderCreatedEvent(order.getId(), order.getTotal());
publisher.publishEvent(event);
return order;
}
@EventListener // обработчик события успешного платежа
@Transactional
public void handlePaymentApproved(PaymentApprovedEvent event) {
Order order = orderRepo.findById(event.getOrderId()).orElseThrow();
order.setStatus(Status.APPROVED);
orderRepo.save(order);
}
@EventListener // обработчик события неудачного платежа
@Transactional
public void handlePaymentFailed(PaymentFailedEvent event) {
Order order = orderRepo.findById(event.getOrderId()).orElseThrow();
order.setStatus(Status.CANCELLED);
orderRepo.save(order);
// (опционально) можно опубликовать событие OrderCancelledEvent для других сервисов
}
}
Здесь метод createOrder
сохраняет заказ и отправляет событие OrderCreatedEvent
. Два метода с аннотацией @EventListener
будут вызываться Spring Context автоматически, когда появятся события соответствующих типов (в реальной системе это может быть сообщение из Kafka; для простоты мы используем ApplicationEventPublisher, что более пригодно для монолита, но концепция схожа).
PaymentService – слушает событие OrderCreated
и публикует исход события платежа:
@Service
public class PaymentService {
@Autowired
PaymentRepository payRepo;
@Autowired
ApplicationEventPublisher publisher;
@EventListener
@Transactional
public void handleOrderCreated(OrderCreatedEvent event) {
// выполнить локальную транзакцию платежа
Payment payment = new Payment(event.getOrderId(), event.getAmount());
boolean success = tryReserveFunds(payment);
payRepo.save(payment);
if (success) {
// сформировать событие об успешном платеже
publisher.publishEvent(new PaymentApprovedEvent(event.getOrderId()));
} else {
// сформировать событие об отказе в платеже
publisher.publishEvent(new PaymentFailedEvent(event.getOrderId()));
}
}
}
Когда PaymentService получает событие о создании заказа, он начинает свою транзакцию – пытается зарезервировать средства (например, уменьшить баланс счета или создать запись транзакции со статусом “Pending”). В нашем примере метод tryReserveFunds
вернет false
, если, скажем, средств недостаточно. После этого PaymentService сохраняет объект Payment (мы могли бы сохранить независимо от успеха, зафиксировав попытку). Далее он публикует либо PaymentApprovedEvent
, либо PaymentFailedEvent
. Эти события, как мы видели, обработает OrderService и обновит статус заказа.
Обратите внимание: отмена и компенсация в хореографии реализуется через такие же события. В нашем случае, если платеж не удался, OrderService помечает заказ отмененным – этого достаточно, потому что других действий (например, разблокировать деньги) не требуется, деньги и так не были зарезервированы. Но предположим более сложный сценарий: OrderService после создания заказа не только запустил PaymentService, но и InventoryService на резерв товара. Если InventoryService успешно зарезервировал товар, а PaymentService затем вернул PaymentFailedEvent
, нужно отменить резерв товара. Как это сделать? Можно предусмотреть, что InventoryService тоже слушает событие OrderCancelled
или PaymentFailed
. Например, OrderService после получения PaymentFailedEvent
публикует OrderCancelledEvent
. InventoryService, услышав его, выполняет локальную компенсирующую транзакцию – освобождает товар на складе. Таким образом, через события можно orchestrate и компенсации: провал одного участника вызывает cascade событий отмены для других. Это требует тщательного проектирования (чтобы каждый сервис знал, как реагировать на отмену).
Идемпотентность и консистентность. В приведенном коде каждый сервис работает в собственной транзакции и общение через события обеспечивает eventual consistency. Очень важно, чтобы обработчики событий были идемпотентными. Например, handlePaymentApproved
в OrderService меняет статус на Approved. Если вдруг по какой-то причине два одинаковых события поступят (дубль), второй раз Order уже будет в Approved, но повторное присвоение этого статуса не должно навредить. Мы должны либо проверять текущее состояние перед применением, либо иметь уникальные идентификаторы событий и игнорировать дубликаты. Также, если OrderService упал после отправки OrderCreatedEvent
и перезагрузился – PaymentService все равно обработает событие и отправит PaymentApproved/Failed, которые OrderService поймает уже после рестарта. Его слушатели handlePaymentApproved/Failed
должны быть готовы к такому позже поступившему сигналу. В целом, проектируя хореографию, нужно учесть, что события доставляются асинхронно и могут приходить не сразу или повторно.
Связь через брокер. В коде для простоты показан механизм Spring Events (в рамках одного процесса). В реальной распределенной системе нужно использовать брокер сообщений. С Spring Cloud Stream можно написать подобные методы с аннотацией @StreamListener
или @KafkaListener
вместо @EventListener
. Публикация события сводится к отправке сообщения в топик Kafka. Например, можно интегрировать publisher.publishEvent
с Kafka-шлюзом, или напрямую использовать kafkaTemplate.send("orders", event)
. Концепция остается: сервисы обмениваются сообщениями, а Spring (или другая библиотека) разруливает сериализацию/десериализацию и вызов методов слушателей. Главное – обеспечить надежную доставку. Шаблон Transactional Outbox тут тоже пригодится: как сделать так, чтобы OrderCreatedEvent публиковалось только если заказ сохранился? Один из способов – сохранять событие во временную таблицу вместе с заказом и иметь фоновый процесс, публикующий из нее в Kafka (или использовать возможности Kafka Transactional Producer). Подобные технические детали выходят за рамки данного примера, но в индустриальной реализации обязательно учитываются для гарантии, что ни одно событие не потеряется и не будет лишним.
Результат работы примера
В результате описанной реализации мы получили непрерывную бизнес-транзакцию через два сервиса. Например, клиент вызывает REST эндпоинт OrderService /orders
– тот создает заказ и (в оркестровочном случае) внутри же вызывает PaymentService и ждет ответа. Клиент может получить ответ сразу после выполнения всех шагов (синхронно), либо OrderService может сразу вернуть статус заказа (Pending) и закрыть соединение, а уведомление о завершении саги отправить клиенту другим способом (например, через WebSocket). В choreography-варианте клиентский запрос к OrderService вернет ответ, как только заказ создан (еще Pending), а дальнейшее подтверждение/отмена произойдет асинхронно. Клиент может опрашивать состояние заказа по API или получать событие об изменении статуса. Оба подхода допустимы, выбор зависит от требований UX.
Ключевые моменты, на которые следует обратить внимание в коде Saga-реализации:
- Управление транзакциями: используем @Transactional в каждом сервисе для локальных операций. Коммит в каждом сервисе фиксирует его часть данных независимо. При оркестрации важно понимать, что все локальные транзакции успешных шагов останутся зафиксированными, даже если финальный шаг провалился – поэтому и нужны компенсирующие транзакции, чтобы логически отменить эффекты.
- Откат операций: в нашем коде компенсация – это вызов
cancelPayment
и отмена заказа. Нужно явно программировать эти действия. Например,cancelPayment
в PaymentService может найти запись платежа и пометить ее refunded, или сделать новый противоположный платеж. Компенсация не всегда тривиальна: если действие необратимо (отправили email, начислили бонусы), приходится предусмотреть семантический откат (отправить «письмо-извинение», скорректировать баланс и пр.). - Обработка ошибок: в оркестраторе используем try/catch, в хореографии – публикацию событий об ошибках. Желательно различать сбои технические (например, недоступность сервиса) и бизнес-ошибки (недостаточно средств, превышен лимит и т.д.). Технические сбои часто решаются ретраем: например, оркестратор может повторить вызов PaymentService 3 раза с интервалом, прежде чем сдаваться. В хореографии ретрай может делать сам сервис-потребитель события (напр., PaymentService может несколько раз пытаться связаться с банковским шлюзом прежде чем сгенерировать
PaymentFailedEvent
). В нашем примере для краткости это опущено, но на практике рекомендуется использовать шаблоны Retry и Circuit Breaker для внешних вызовов.
Обработка ошибок и отказов в Saga
Проектируя Saga для платежной (и любой финансовой) системы, необходимо предусмотреть множество потенциальных ошибок и аномальных ситуаций. Ниже перечислены некоторые из важных аспектов надежности Saga и способы их обработки:
- Гарантия завершения или отката. Сага должна гарантировать, что либо все локальные транзакции выполнились, либо при сбое все уже выполненные операции компенсированы, чтобы система вернулась в согласованное состояние. Иными словами, допустимы только два исхода: успех всей саги либо полный откат. На практике, однако, возможны ситуации, когда не удается сразу достичь ни того, ни другого. Например, произошла ошибка в процессе отката – один из компенсирующих шагов тоже провалился. В таком случае данные могут временно остаться в неконсистентном виде. Как быть? Необходимо предусмотреть механизм выявления таких ситуаций (например, саги, застрявшие в промежуточном состоянии дольше заданного времени) и их ручного или автоматического доведения до консистентности. Часто внедряют мониторинг саг: если спустя N минут сага не завершилась, она помечается как “ERROR” и выносится на ручную обработку. Компенсирующие транзакции сами по себе должны быть максимально надежными: например, их можно тоже повторять несколько раз при временных сбоях. Если же компенсация окончательно не удалась (скажем, деньги списались, а вернуть на карту не получилось автоматически), система должна эскалировать проблему во внешнюю систему (техническую поддержку, финансовый отдел) для внеочередного разбора.
- Потеря сообщения или двойная запись. В распределенных системах велика вероятность сетевых сбоев. Что, если событие Saga потеряется в брокере или вызов оркестратора не дойдет до сервиса? Для обеспечения надежности используют транзакционную передачу сообщений. Шаблон Transactional Outbox, упомянутый ранее, гарантирует, что событие будет опубликовано только вместе с фиксацией изменения в базе. Если сервис падает, не отправив сообщение, его можно отправить позже, прочитав из outbox-таблицы. С другой стороны, если сервис не получил сообщение (например, PaymentService не получил OrderCreatedEvent из-за перебоя сети), обычно помогают механизмы повторной доставки на уровне брокера (Kafka сохраняет сообщения, и потребитель прочитает их после восстановления) или же оркестратор ставит таймаут: не получил ответ – повторяет команду. Важное требование – идемпотентность операций. Повторный запуск шага или дублирующее событие не должны приводить к неправильному двойному эффекту. Для этого часто применяют уникальные идентификаторы операций: каждый шаг саги помечается ID, и сервис-потребитель прежде чем выполнить действие, проверяет, не выполнялся ли уже такой ID (например, PaymentService может хранить у себя флаг “платеж для orderId=X уже резервировался” и игнорировать повторный запрос). Идемпотентность и ретраи позволяют сгладить временные сбои сети.
- Отсутствие изоляции и аномалии данных. Saga, в отличие от ACID-транзакции, не обеспечивает изоляции между шагами. Это значит, что пока Saga не завершилась, внешние системы или параллельные Saga могут видеть промежуточные данные. Например, заказ может быть в статусе Pending, или деньги на счете временно зарезервированы, и другой процесс может тоже попытаться их зарезервировать. Такие аномалии как dirty reads, lost updates, inconsistent reads возможны. Для критичных сценариев следует продумывать контрмеры: семантические блокировки (помечать ресурс как “в процессе изменения” и не позволять другим сагам его трогать), версирование данных (проверять версию объекта перед записью, чтобы не перетереть изменения из параллельной саги), либо конфигурация порядка шагов так, чтобы потенциальные конфликты случались либо в начале, либо в конце саги (паттерн pessimistic view, когда все необратимые действия с побочными эффектами выносятся на финал). В платежных системах, например, можно блокировать счет клиента на время выполнения саги (семафор на уровне приложения), чтобы параллельные операции не нарушили баланс.
- Компенсация, которая не может быть выполнена автоматически. Как упоминалось, не все действия обратимы. Если Saga включает шаг, который нельзя отменить технически, нужно реализовать семантическую компенсацию. Пример: отправленное уведомление на email нельзя “отозвать”, но можно отправить следом письмо с извинением, нивелирующее эффект. В финансовых системах возможны сценарии, когда деньги ушли во внешний банк – вернуть автоматически невозможно, нужна отдельная операция возврата, которая может быть выполнена уже вне текущей саги (новая сага или ручной процесс). Поэтому при проектировании Saga рекомендуется избегать необратимых шагов в середине процесса. Если уж они нужны, их стараются делать либо самыми последними (чтобы уж если дошло – то откат не предусмотрен, считается все успешно), либо первыми (как pivot – точка невозврата, после которой вся остальная логика должна уже гарантированно выполниться). Например, часто платеж списывается последним шагом – после того, как все проверки пройдены и товар точно есть. Или наоборот – сначала списываем деньги (pivot), а потом, если товар не смогли отгрузить, делаем новый полноценный платеж-возврат (что уже отдельная финансовая транзакция, не совсем компенсация внутри той же саги).
- Мониторинг и трассировка. В продакшне жизненно важно иметь наблюдаемость за Saga. Нужно логировать события саги, иметь корреляционные идентификаторы (например, SagaID, orderId) во всех сообщениях и логах, чтобы можно было собрать полную цепочку. Интеграция с системами трассировки (Jaeger, Zipkin) облегчит отладку. Можно реализовать таблицу состояний Saga: оркестратор или каждый участник пишет в общую таблицу текущее состояние саги (например, SagaID, шаг1=OK, шаг2=Failed и т.п.), тогда админы и поддержка смогут быстро обнаружить “застрявшие” или неуспешные саги и разобраться. Автоматические алерты на долго незавершающиеся саги помогают проактивно выявлять проблемы (например, зависшую службу, которая не присылает событие).
- Использование фреймворков. Реализация Saga – задача нетривиальная, поэтому для критичных платежных систем рассматривают готовые решения: оркестровщики процессов (Camunda, Zeebe), фреймворки Saga (Axon, Eventuate) или системы распределенных транзакций. Такие инструменты могут брать на себя часть упомянутых сложностей – сохранение контекста, гарантированную доставку сообщений, ретраи, идемпотентность, мониторинг. Например, Temporal (от Uber) позволяет писать оркестрацию на привычном коде, а платформа гарантирует, что ни один шаг не потеряется: при сбое workflow возобновится с нужного места, что упрощает разработку. Spring Boot не имеет из коробки Saga-менеджера, но Spring Cloud позволяет интегрировать Stateful Saga паттерны с помощью Spring State Machine или использовать Spring Cloud Data Flow для оркестрации. Выбор подхода – оставить логику на приложениях или внедрить отдельный оркестровщик – зависит от масштаба системы. В небольших системах ручная реализация Saga может быть оправдана, в крупных – лучше задействовать проверенные временем решения, чтобы снизить риск ошибок в транзакционной логике.
Заключение
Паттерн Saga стал де-факто стандартом для обеспечения согласованности в распределенных транзакциях, особенно в финансовых и платежных микросервисах. Он требует пересмотра подхода к проектированию: вместо жестких ACID-транзакций разработчик думает в терминах событий, компенсаций и конечной согласованности данных. Мы рассмотрели две основные стратегии координации Saga – хореографию и оркестрацию – а также их применение на примере платежной системы. Каждая стратегия имеет свои достоинства и недостатки: хореография обеспечивает слабую связанность сервисов и простоту, но усложняет поддержку, оркестрация дает централизованный контроль ценой дополнительной сложности и точки отказа. В реальных системах выбор нередко определяется масштабом и критичностью бизнес-процесса: для простых платежей (например, только списать деньги и уведомить) подойдет и событийная схема, а для комплексных (например, каскадное списание, расчеты, уведомления нескольким участникам) зачастую лучше явно оркестровать.
При правильной реализации Saga позволяет достичь надежности и консистентности, близких к монолитным транзакциям, но с сохранением всех преимуществ микросервисной архитектуры – масштабируемости, автономности компонентов и гибкости развития. Главное – уделить должное внимание обработке ошибок, компенсирующим действиям и наблюдаемости, чтобы ваша распределенная платежная система оставалась корректной даже перед лицом сбоев. Saga-паттерн, дополняемый такими практиками как идемпотентность, транзакционный outbox, мониторинг – это мощный инструмент, позволяющий строить устойчивые распределенные финтех-системы, обеспечивая целостность данных клиентов и бизнеса без жесткой связки сервисов.
You must be logged in to post a comment.