Распределённые транзакции между независимыми сервисами невозможно реализовать одной транзакцией базы данных — для этого применяют паттерн Saga с заранее продуманными компенсациями на случай ошибок. Подход Durable Execution позволяет длительному процессу пережить сбои и восстановить выполнение, а в статье на примере продукта Durable Workflow для PHP и Laravel разбирается, как организовать надёжные компенсации и управлять состоянием.
Для таких процессов применяют Saga: последовательность локальных операций с заранее продуманными действиями на случай ошибки. А чтобы длительный процесс мог продолжиться после сбоя, используют подход Durable Execution. Разберём, как эти идеи дополняют друг друга и как организовать компенсации в Laravel на примере продукта Durable Workflow.
Что такое Saga и какую проблему она решает
Обычная транзакция базы данных объединяет изменения в пределах поддерживаемой ей границы: они либо фиксируются, либо откатываются. Но платёжный шлюз, склад и служба доставки обычно работают в разных системах. Общая ACID-транзакция для них может быть недоступна или слишком сложна в эксплуатации.
Saga разбивает бизнес-процесс на локальные транзакции. Изменения каждого шага фиксируются независимо от остальных. Если процесс не может продолжиться, система запускает предусмотренные бизнес-логикой компенсации: например, освобождает резерв или оформляет возврат платежа.
Важно: компенсация — не то же самое, что откат базы данных. Возврат денег может занять несколько дней, а уже отправленное письмо нельзя «отправить обратно». Иногда вместо полного восстановления исходного состояния нужна другая операция: отмена заказа, уведомление клиента или ручная обработка.
Во время выполнения другие участники могут видеть промежуточное состояние. Saga не обеспечивает общей атомарности и изоляции ACID-транзакции; она помогает привести систему к согласованному бизнес-состоянию при условии, что ошибки и компенсации обработаны. Подробнее об ограничениях — в описании паттерна AWS.
Пример: заказ, оплата и доставка
Рассмотрим условный интернет-магазин. Это иллюстрация паттерна, а не описание конкретного внедрения.
| Шаг | Основное действие | Возможная компенсация |
| 1 | Создать заказ в статусе «Ожидает обработки» | Отменить заказ |
| 2 | Зарезервировать товар | Освободить резерв |
| 3 | Списать оплату | Инициировать возврат |
| 4 | Создать отправление | Отменить отправление, если перевозчик это допускает |
Если отправление не удалось создать после подтверждённого списания, процесс может инициировать возврат, освободить товар и отменить заказ. При последовательной компенсации порядок обычно обратный: сначала отменяем более поздние действия, затем ранние. Если возврат или отмена недоступны, заказу нужен отдельный статус и вмешательство оператора.
На схеме показаны основные действия и возможные компенсации. При ошибке запускают не все компенсации подряд, а те, которые соответствуют уже выполненным шагам.

Схема 1. Условный процесс заказа и его компенсации.
Оркестрация и хореография
Есть два распространённых способа координировать участников:
- Оркестрация: отдельный координатор управляет шагами, ожиданиями и компенсациями. Весь сценарий проще проследить в одном месте, но сам координатор нужно надёжно хранить и обслуживать.
- Хореография: сервисы реагируют на события друг друга. Центрального сценария нет, но с ростом числа участников становится сложнее видеть зависимости, отлаживать тайм-ауты и отслеживать весь процесс.
При хореографии сервисы запускают следующие шаги через события: например, склад реагирует на создание заказа, а платёжный сервис — на резервирование товара.

Схема 2. Хореография: участники реагируют на события друг друга.
При оркестрации участники получают команды от координатора и сообщают ему результат. Координатор решает, продолжить процесс или запустить компенсации.

Схема 3. Оркестрация: координатор управляет взаимодействием сервисов.
Выбор зависит от системы: для нескольких независимых участников может подойти хореография; для длинного процесса с условиями, ожиданиями и компенсациями часто удобнее явный координатор. AWS разбирает компромиссы хореографии. Схемы выше созданы для этой статьи; общие принципы также описаны в материале AWS о Saga.
Когда использовать Saga, а когда достаточно очереди
Saga полезна, если операция затрагивает несколько независимых систем, частичный успех создаёт проблему, а для выполненных действий можно определить приемлемые компенсации. Ожидание подтверждения, длительная обработка и необходимость разбирать историю выполнения дополнительно повышают ценность workflow-движка.
Но отдельный движок нужен не всегда:
- Одна база и одна граница транзакции — начните с обычной транзакции БД.
- Простая последовательность фоновых задач — рассмотрите Laravel Jobs и цепочки заданий.
- Несколько систем с компенсируемыми действиями — рассмотрите Saga; координатор можно реализовать самостоятельно или использовать workflow-движок.
Корректно настроенная очередь Laravel не обязана терять задания при перезапуске воркера: она поддерживает постоянные драйверы, повторные попытки и обработку ошибок. Разница в другом: история всего бизнес-процесса, ожидание внешних сигналов и согласованный запуск компенсаций в цепочках заданий потребуют дополнительной реализации. См. документацию Laravel Queues.
Что такое Durable Execution и как он помогает Saga
Durable Execution — подход к выполнению программ, при котором процесс может продолжиться после сбоя, не теряя зафиксированный прогресс. Как объясняет Temporal, выполнение не привязано к одному процессу или машине: после сбоя платформа восстанавливает состояние и продолжает работу. Это полезно и для длительных процессов, которые часами или днями ждут события, подтверждения или таймера.
Durable Execution — общий принцип, а не название конкретного продукта. Его реализации различаются; не каждый workflow-движок обеспечивает такое выполнение.
Durable Workflow, который используется в примерах статьи, — отдельный продукт, реализующий этот подход.
Saga решает другую задачу: определяет локальные шаги и компенсации для достижения согласованного бизнес-результата.
Durable Execution помогает сохранять и восстанавливать выполнение Saga, но не придумывает компенсации и не гарантирует их успех.
В Durable Workflow сохраняется история выполнения процесса. При восстановлении код workflow воспроизводится по этой истории, а результаты уже зафиксированных шагов используются повторно. Внешнюю работу — запросы к API, запись в другую систему, платёж — выносят в activities.
При этом история движка не означает, что внешний платёж или бронь физически выполнятся ровно один раз. Попытка activity может повториться после сбоя, поэтому внешние операции и их компенсации нужно проектировать с учётом повторных вызовов.
О различиях между повторным выполнением и воспроизведением истории рассказывает руководство PHP SDK.
У продукта есть два разных режима:
- Embedded Laravel — workflow работает внутри Laravel-приложения. Пример ниже использует API
Workflow\V2из документации embedded-режима. - Отдельный Durable Workflow Server — сервер хранит состояние и координирует воркеры через свой протокол; PHP, Python и Rust подключаются через свои SDK. В примере ниже используется встроенный режим: компенсации регистрируются через
$this->addCompensation(). В серверном режиме PHP SDK использует другой способ —$context->saga(), поэтому для него пример нужно адаптировать.
Пример Saga в Laravel: бронирование поездки
Возьмём сценарий из документации Durable Workflow: последовательно бронируем перелёт, отель и автомобиль. После каждого успешного шага регистрируем действие для его отмены.
Это фрагмент workflow, а не готовое приложение: классы бронирования и отмены нужно реализовать самостоятельно, установить и настроить совместимую версию embedded-пакета, хранилище и обработчики. Перед внедрением сверьте API с документацией своей версии.
use function Workflow\V2\activity;
use Workflow\V2\Attributes\Type;
use Workflow\V2\Workflow;
#[Type('booking-saga')]
class BookingSagaWorkflow extends Workflow
{
public function handle(): array
{
try {
$flightId = activity(BookFlightActivity::class);
$this->addCompensation(
fn () => activity(CancelFlightActivity::class, $flightId)
);
$hotelId = activity(BookHotelActivity::class);
$this->addCompensation(
fn () => activity(CancelHotelActivity::class, $hotelId)
);
$carId = activity(BookRentalCarActivity::class);
$this->addCompensation(
fn () => activity(CancelRentalCarActivity::class, $carId)
);
return compact('flightId', 'hotelId', 'carId');
} catch (\Throwable $e) {
$this->compensate();
throw $e;
}
}
}
Пример компенсации: отмена брони отеля
В предыдущем примере compensate() не отменяет бронь сам по себе: он вызывает зарегистрированную CancelHotelActivity. Ниже — упрощённая реализация этой activity для локальной базы приложения.
Предположим, что BookHotelActivity создаёт запись HotelReservation со статусом confirmed и возвращает её идентификатор. У модели есть поля id и status. Тогда компенсация может выглядеть так:
<?php
namespace App\Workflows\Activities;
use App\Models\HotelReservation;
use RuntimeException;
use Workflow\V2\Activity;
final class CancelHotelActivity extends Activity
{
public function handle(int $hotelId): void
{
// Меняем статус только у подтверждённой брони.
$updated = HotelReservation::query()
->whereKey($hotelId)
->where('status', 'confirmed')
->update(['status' => 'cancelled']);
if ($updated === 1) {
return;
}
$status = HotelReservation::query()
->whereKey($hotelId)
->value('status');
// Повторная отмена уже отменённой брони безопасна.
if ($status === 'cancelled') {
return;
}
throw new RuntimeException(
"Не удалось отменить бронь отеля: {$hotelId}"
);
}
}
Здесь handle() — точка входа activity в embedded Durable Workflow V2; CancelHotelActivity нужно импортировать в файл workflow. Условное обновление не отменит бронь с неожиданным статусом, а повторный вызов для уже отменённой записи завершится без ошибки. Если запись не найдена или её статус не позволяет отмену, activity сообщает об ошибке: её нельзя считать успешно выполненной.
Это пример локальной компенсации. Если бронь создана у внешнего поставщика, одного изменения статуса в нашей базе недостаточно: activity должна вызвать API отмены, использовать поддерживаемый поставщиком ключ идемпотентности и проверить результат. Если ответ потерялся, потребуется выяснить статус брони у поставщика. Повторные попытки и действия при окончательной ошибке нужно настроить отдельно.
Подробнее о структуре activity — в документации Durable Workflow.
Что произойдёт при ошибке
Если бронирование автомобиля завершится ошибкой после успешной брони перелёта и отеля, compensate() сначала вызовет отмену отеля, затем перелёта. Компенсация автомобиля не зарегистрирована: его шаг не вернул успешный результат. При успешном завершении workflow компенсации не вызываются.
По умолчанию компенсации выполняются последовательно в обратном порядке. Если одна из них окончательно завершится ошибкой, выполнение оставшихся остановится, а ошибка компенсации распространится наружу. Поэтому строка throw $e в примере сработает только после успешной компенсации: иначе исходную ошибку нужно сохранять и анализировать вместе с ошибкой отмены.
Сама регистрация не запускает откат автоматически: его явно вызывает код в catch. Отмена также требует работающих обработчиков и достаточного времени на выполнение и повторные попытки.
Что если ответ от внешнего API потерялся
В примере компенсация регистрируется после успешного ответа activity. Но возможна неоднозначная ситуация: отель создал бронь, ответ потерялся, а activity завершилась ошибкой. Workflow не получил hotelId и не зарегистрировал отмену, хотя побочный эффект уже существует.
Для таких случаев нужен отдельный протокол восстановления: стабильный бизнес-ключ бронирования, идемпотентные повторные запросы, проверка статуса у провайдера и сверка неизвестных результатов. Если API поддерживает безопасную отмену по заранее известному ключу, можно рассмотреть регистрацию компенсации до вызова; она должна корректно обрабатывать отсутствие брони. Это не автоматическая гарантия приведённого примера. Temporal разбирает оба момента регистрации и их последствия.
Как спроектировать надёжные компенсации
Идемпотентность и повторные попытки
Повторный вызов должен приводить к тому же допустимому результату, что и первый. Например, повторная отмена уже снятого резерва не должна освобождать чужой товар, а повторный запрос на возврат — создавать второй возврат.
Практический подход:
- Присвойте заказу или бизнес-операции стабильный идентификатор и сформируйте отдельный ключ для каждого действия и компенсации.
- Передавайте ключ внешнему сервису, если он поддерживает идемпотентность, и сохраняйте связь между ключом и результатом у себя.
- Настройте ограниченные повторные попытки для временных ошибок и отдельную обработку постоянных отказов.
- Проверяйте неопределённые результаты через API провайдера или процедуру сверки. Локальный ключ сам по себе не гарантирует защиту, если внешний сервис его не учитывает.
Порядок и ошибки компенсации
Последовательная отмена в обратном порядке подходит, когда поздние действия зависят от ранних. В embedded-режиме можно включить параллельную компенсацию через setParallelCompensation(true), но только если отменяемые действия действительно независимы. У этого режима есть требования к возвращаемым из замыканий activity; проверьте пример в документации.
Настройка setContinueWithError(true) позволяет продолжить попытки компенсации после ошибки одного шага. По документации embedded-режима исключения отдельных компенсаций при этом перехватываются и отбрасываются. Такой режим не означает полного успеха: предусмотрите отдельную фиксацию результата, уведомления и ручную обработку незавершённых отмен. Не включайте его бездумно, если следующие компенсации зависят от результата предыдущей.
Тайм-ауты и отмена процесса
Различайте бизнес-дедлайн и тайм-аут движка. Например, бронь должна ждать подтверждения 30 секунд, после чего workflow сам запускает освобождение ресурса. Общий лимит выполнения при этом должен быть достаточно большим, чтобы осталось время на компенсацию и повторы.
В embedded Durable Workflow истечение общего workflow timeout закрывает выполнение до того, как код сможет запустить очистку; обычный catch или finally этого не исправляет. Немедленные cancel() и terminate() также не дают окна для компенсации. Для управляемой отмены используйте предусмотренный версией механизма cooperative cancellation и сверяйтесь с документацией Saga.
Конкурентные процессы и события
Saga не изолирует промежуточные изменения от других процессов. Два заказа могут одновременно претендовать на последний товар, поэтому резервирование должно проверять доступность и фиксировать резерв атомарно в системе склада. В зависимости от задачи используют условные обновления, версии записей или бизнес-блокировки.
Если участник меняет данные и одновременно должен опубликовать событие, возникает проблема двойной записи: база обновилась, а сообщение не отправилось. Один из вариантов — transactional outbox: сохранить изменение и запись о событии в одной локальной транзакции, а затем отдельно доставить событие. Получатель всё равно должен учитывать возможные дубликаты. Подробности и ограничения описаны в AWS Transactional Outbox.
Детерминированность и наблюдаемость
Код workflow должен принимать совместимые решения при воспроизведении сохранённой истории. Внешние вызовы и другие побочные эффекты размещайте в activities; изменения долгоживущих workflow проверяйте на совместимость с уже запущенными экземплярами и возможностями версионирования выбранного режима.
Для эксплуатации сохраняйте идентификатор процесса и бизнес-операции, текущий шаг, состояние компенсаций и ссылки на связанные события. Настройте оповещения о зависших процессах и окончательных ошибках, а также порядок ручной сверки. История в интерфейсе помогает разбирать сбои, но сама по себе не исправляет ошибку у платёжного или другого внешнего провайдера.
Durable Workflow Server 3.0: что изменилось
3 ноября 2026 года команда представила Server 3.0 — отдельный сервер на Rust. По описанию разработчиков, его серверная часть работает одним процессом с базой данных: самостоятельно создаёт схему и обслуживает таймеры и расписания. Поддерживаются SQLite, MySQL, MariaDB и PostgreSQL.
Это упрощает развёртывание серверной части: рядом с ней больше не нужны отдельные Redis, серверный queue worker и scheduler из прежней архитектуры. Но база, резервное копирование, аутентификация и SDK-воркеры приложения по-прежнему нужны. Embedded Laravel при этом остаётся отдельным вариантом и не заменяется Server 3.0.
Для перехода с PHP Server 2.x разработчики описывают необратимую миграцию с предварительной проверкой резервной копии. В релизной статье также отмечены ограничения, включая отсутствие опубликованного подтверждения отказоустойчивости native Server и серверных квот. Перед обновлением проверьте матрицу поддержки и совместимые версии SDK, а миграцию сначала выполните на копии.
Проверка перед внедрением
- Для каждого шага определены результат, допустимая компенсация и ситуации, когда нужен оператор.
- Основные действия и компенсации безопасны при повторах; известен способ выяснить результат после потери ответа.
- Порядок компенсаций соответствует зависимостям; параллельное выполнение используется только для независимых действий.
- У повторов, ожиданий и компенсаций есть тайм-ауты и достаточный запас времени до общего дедлайна.
- Конкурентные операции защищены на стороне владельца данных; публикация событий не теряется при двойной записи.
- Видны состояние процесса и ошибки; есть уведомления, сверка и процедура ручного восстановления.
- На тестовом окружении проверены отказ каждого шага, ошибка компенсации, повторный вызов, потеря ответа и перезапуск воркера. Изменения workflow проверены на совместимость с уже запущенными процессами.
Итог
Saga помогает управлять бизнес-процессом между независимыми системами, но не превращает его в одну ACID-транзакцию. Надёжность складывается из нескольких частей: понятных компенсаций, идемпотентных операций, обработки неопределённых результатов, контроля конкурентности и наблюдаемости.
Durable Execution позволяет продолжать выполнение после сбоев и управлять длительными ожиданиями. В примере статьи этот подход реализован с помощью Durable Workflow, который сохраняет историю и восстанавливает исполнение. Правила возврата платежа, отмены брони и действия при неудачной компенсации остаются ответственностью приложения и команды.
Источники и материалы
- Temporal: The definitive guide to Durable Execution — определение Durable Execution, восстановление после сбоев и длительные процессы.
- Durable Workflow: Sagas в embedded Laravel — пример бронирования, порядок компенсаций, настройки и дедлайны.
- Durable Workflow PHP SDK: workflows и activities — восстановление по истории, идемпотентность и отдельный API для серверного режима.
- Durable Workflow Server 3.0 — устройство сервера, совместимость и ограничения релиза.
- Durable Workflow Sample App: кросс-языковые Saga и перезапуск — экспериментальный пример с PHP, Python и Rust; проверяет порядок истории, но не восстановление внешних побочных эффектов.
- AWS: Saga patterns и Saga choreography — варианты организации процесса и их ограничения.
- AWS: Transactional Outbox — проблема двойной записи и вариант её решения.
- Temporal: Saga Pattern — выбор момента регистрации компенсаций и неоднозначные результаты внешних вызовов.
- Laravel: очереди и цепочки заданий — альтернативный подход для более простых задач.