🏗️ Розділ 17 · Питання #2

У чому різниця між хореографією та оркестрацією в Saga

У розподілених системах патерн Saga замінює класичні ACID-транзакції ланцюжком локальних транзакцій із компенсувальними діями у разі збоїв. Координація цих кроків реалізується д...


🟢 Junior Level

У розподілених системах патерн Saga замінює класичні ACID-транзакції ланцюжком локальних транзакцій із компенсувальними діями у разі збоїв. Координація цих кроків реалізується двома принципово різними підходами: хореографією (Choreography) та оркестрацією (Orchestration).

Хореографія (Event-Driven):
[Order Service] ──OrderCreated──► [Payment Service] ──PaymentCompleted──► [Delivery Service]
     ▲                                   │
     └────────PaymentFailed (Rollback)───┘

Оркестрація (Command-Driven):
                   ┌───────────────────────────┐
                   │    Saga Orchestrator      │
                   │ (State Machine + Saga Log)│
                   └──────┬───────┬───────┬────┘
           1. CreateOrder │       │ 2. Charge │ 3. Dispatch
                          ▼       ▼           ▼
                   [Order Service] [Payment] [Delivery]

Основна метафора:

  • Хореографія схожа на танець на балу: немає ведучого, кожен танцюрист вивчив свою партію і реагує на рухи та жести партнерів. Сервіси обмінюються подіями (OrderCreated, PaymentAuthorized) через брокер повідомлень.
  • Оркестрація схожа на симфонічний оркестр із диригентом: музиканти не дивляться один на одного, вони дивляться на диригента (оркестратор), який явно вказує, кому, коли і яку команду виконати (ReserveInventoryCommand, ChargeCardCommand).

Коротко про ключові відмінності:

  1. Керування: У хореографії керування децентралізоване (немає єдиної точки координації); в оркестрації керування централізоване у спеціальному сервісі/модулі (Saga Execution Coordinator).
  2. Стиль повідомлень: Хореографія базується на доменних подіях (events: «що сталося»), оркестрація — на командах (commands: «що потрібно зробити») та відповідях.
  3. Область застосування: Хореографія ідеальна для коротких ланцюжків (2–3 сервіси). Оркестрація незамінна для складних бізнес-процесів (4+ сервісів, розгалуження, тайм-аути).

🟡 Middle Level

1. Архітектура хореографії (Choreography)

Сервіси реагують на події сусідів, виконують локальну транзакцію у своїй БД і публікують власну подію через Transactional Outbox.

// Сервіс оплати реагує на подію створення замовлення
@Component
public class PaymentEventHandler {

    private final PaymentService paymentService;
    private final EventPublisher eventPublisher;

    @KafkaListener(topics = "order-events", groupId = "payment-group")
    public void onOrderCreated(OrderCreatedEvent event) {
        try {
            paymentService.processPayment(event.orderId(), event.amount());
            // Успіх: публікуємо подію для наступного кроку (Delivery)
            eventPublisher.publish(new PaymentSucceededEvent(event.orderId()));
        } catch (InsufficientFundsException e) {
            // Помилка: публікуємо компенсувальну подію
            eventPublisher.publish(new PaymentFailedEvent(event.orderId(), e.getMessage()));
        }
    }
}

Особливості відкату: Якщо на кроці 3 (Delivery) стається збій, сервіс доставки публікує DeliveryFailedEvent. Сервіс оплати зобов’язаний слухати цю подію та виконати повернення коштів (refund), а сервіс замовлень — скасувати замовлення (CANCELLED).

2. Архітектура оркестрації (Orchestration)

Координатор (Saga Execution Coordinator, SEC) зберігає поточний стан процесу (FSM — Finite State Machine) у персистентному сховищі (Saga Log) і надсилає адресні команди сервісам-учасникам.

@Component
public class OrderSagaOrchestrator {

    private final SagaRepository sagaRepo;
    private final CommandGateway commandGateway;

    @Transactional
    public void onEvent(SagaReplyEvent reply) {
        OrderSagaData saga = sagaRepo.findById(reply.sagaId())
            .orElseThrow();

        switch (saga.getState()) {
            case PAYMENT_PENDING -> {
                if (reply.isSuccess()) {
                    saga.transitionTo(SagaState.DELIVERY_PENDING);
                    commandGateway.send(new ScheduleDeliveryCommand(saga.getOrderId()));
                } else {
                    saga.transitionTo(SagaState.COMPENSATING_ORDER);
                    commandGateway.send(new CancelOrderCommand(saga.getOrderId()));
                }
            }
            case DELIVERY_PENDING -> {
                if (reply.isSuccess()) {
                    saga.transitionTo(SagaState.COMPLETED);
                } else {
                    saga.transitionTo(SagaState.REFUNDING_PAYMENT);
                    commandGateway.send(new RefundPaymentCommand(saga.getOrderId()));
                }
            }
            case REFUNDING_PAYMENT -> {
                saga.transitionTo(SagaState.COMPENSATING_ORDER);
                commandGateway.send(new CancelOrderCommand(saga.getOrderId()));
            }
            case COMPENSATING_ORDER -> saga.transitionTo(SagaState.REJECTED);
        }
        sagaRepo.save(saga);
    }
}

3. Детальне технічне порівняння

Критерій Хореографія (Choreography) Оркестрація (Orchestration)
Стиль комунікації Event-Driven (Pub/Sub: «Замовлення створено») Command-Driven (Point-to-Point: «Спиши кошти»)
Координація Розподілена, закладена в логіку кожного учасника Централізована в Saga Execution Coordinator (SEC)
Зв’язність (Coupling) Слабка за викликами, але висока семантична зв’язність за подіями Центральна зв’язність: оркестратор знає про всі кроки процесу
Спостережуваність (Observability) Низька: щоб зрозуміти статус процесу, потрібно збирати логи з усіх сервісів або використовувати Distributed Tracing Висока: стан будь-якого замовлення видно в одній таблиці/панелі оркестратора
Циклічні залежності Високий ризик створення «зачарованого кола» подій ($A \to B \to C \to A$) Виключені: граф переходів станів строго спрямований (DAG)
Точка відмови (SPOF) Відсутній єдиний керуючий сервіс Оркестратор є критичним компонентом (вимагає кластеризації та реплікації БД)
Складність змін При додаванні кроку доводиться переналаштовувати підписки кількох сервісів Достатньо змінити граф станів в одному оркестраторі

🔴 Senior Level

1. Архітектурні ризики та пастки

А. Хореографія: «Spaghetti Architecture» та прихований розподілений моноліт

При зростанні системи (10+ сервісів) хореографія призводить до ситуації, коли жоден інженер не здатен передбачити повний шлях обробки запиту без побудови графа залежностей через Jaeger/Zipkin. Зміна схеми однієї події (OrderCreatedEvent) ламає 5 незалежних сервісів-підписників. Виникають цикли: ServiceA породжує подію, яка через 3 сервіси повертається до ServiceA як стимул для нової транзакції.

Б. Оркестрація: «Smart Orchestrator, Anemic Microservices»

Найчастіша помилка при оркестрації — перенесення бізнес-логіки сервісів в оркестратор. Мікросервіси деградують до примітивних CRUD-виконавців, а оркестратор перетворюється на роздутий «божественний об’єкт» (God Object), що вимагає деплою при кожній найменшій зміні правил валідації чи розрахунку знижок. Правило: Оркестратор повинен керувати лише послідовністю кроків (Workflow & State), але не ухвалювати доменних рішень про те, як резервувати товар.

2. Гонки повідомлень та несинхронне отримання (Out-of-Order Delivery)

У хореографії при паралельній обробці черг можлива рассинхронізація:

  • Сервіс доставки може отримати OrderCancelledEvent (компенсація) раніше, ніж до нього дійшло вихідне OrderCreatedEvent (наприклад, через мережеву затримку або ребалансування партиції Kafka).
  • Рішення: State-based ідемпотентність та буферизація. Якщо приходить скасування для неіснуючого замовлення, сервіс фіксує запис: Order { id: 123, state: CANCELLED_PREMATURELY }. Коли пізніше прийде вихідний CreateOrder, сервіс побачить статус і просто проігнорує створення.
Хронологія гонки (Race Condition):
1. [Broker] ────OrderCancelled──► [Delivery] (замовлення створено зі статусом CANCELLED_PREMATURELY)
2. [Broker] ────CreateDelivery──► [Delivery] (виявлено прапорець скасування -> дія пропускається)

3. Промислові рушії оркестрації

Писати саморобний оркестратор на switch-case допустимо лише для простих завдань. У Highload- та Enterprise-системах застосовуються спеціалізовані рушії:

  1. Temporal.io / Cadence:
    • Підхід: Code-as-Configuration. Вся сага пишеться як звичайний детермінований Java-код (Workflow.sleep(), цикли, try-catch).
    • Під капотом: Event Sourcing. Рушій перехоплює виклики активностей, зберігає історію подій у БД і при падінні ноди відтворює стан потоку з точністю до рядка коду.
  2. Camunda 7 / 8 (Zeebe):
    • Підхід: BPMN 2.0. Бізнес-процес моделюється графічно в нотації BPMN, підтримуються Compensation Boundary Events, тайм-аути, ручні ескалації.
    • Zeebe розподіляє партиції подій через Raft, забезпечуючи мільйонну пропускну здатність (throughput).
  3. Axon Framework:
    • Вбудований компонент @Saga з асоціацією за ключами (associationProperty = "orderId").

4. Гібридний підхід (Pragmatic Architecture)

У зрілих архітектурах використовують гібрид:

  • Оркестрація всередині Bounded Context: Усередині домену замовлень складний чекаут (резерв бонусів, бронь складу, перевірка антифроду) оркеструється внутрішнім SEC.
  • Хореографія між Bounded Contexts: По завершенні саги чекауту публікується публічна доменна подія OrderConfirmedEvent, на яку незалежно реагують контексти аналітики, сповіщень та рекомендацій.

4 Tricky Questions

1. Що станеться, якщо вузол з оркестратором аварійно завершиться (kill -9) під час виконання саги?

Відповідь: Якщо оркестратор спроєктований правильно, він зберігає стан переходу в Saga Log до відправлення команди (або використовує Event Sourcing). При рестарті екземпляра, що впав, або перехопленні партиції іншим вузлом кластера:

  1. Завантажується останній зафіксований стан із Saga Log.
  2. Перевіряється статус виконання завислої команди (за тайм-аутом або опитуванням статусу сервісу).
  3. Команда відправляється повторно. Завдяки ідемпотентності сервісу-одержувача повторна команда не призведе до дублювання дій. Без персистентного Saga Log оркестратор, що впав, залишає розподілену транзакцію у вічно «завислому» неузгодженому стані.

2. Чому в хореографії значно складніше реалізувати тайм-аути кроків (SLA) та ескалацію?

Відповідь: В оркестрації тайм-аут реалізується централізовано: оркестратор зводить таймер (наприклад, у Temporal Workflow.await(Duration.ofMinutes(5)) або Dead Letter Scheduler) і, якщо відповідь не отримана, запускає компенсувальні транзакції. У хореографії немає єдиного власника часу:

  • Або кожен сервіс повинен реалізовувати власні складні таймери та публікувати події StepTimeoutEvent.
  • Або доводиться виділяти окремий допоміжний сервіс моніторингу SLA, який фактично перетворюється на прихований оркестратор.

3. Що таке «фантомна компенсація» (Phantom Compensation) і як від неї захиститися?

Відповідь: Фантомна компенсація виникає, коли компенсувальна дія виконується для транзакції, яка насправді ніколи не виконувалася на цільовому сервісі. Приклад: Оркестратор відправив команду ReserveInventory, але запит застряг у мережі. Оркестратор за тайм-аутом вирішує відкотити сагу і шле ReleaseInventory. Сервіс складу отримує ReleaseInventory, збільшує залишок на складі на +1, а потім із затримкою отримує вихідний ReserveInventory. Захист:

  1. Використання унікального SagaId / TransactionId на всіх кроках.
  2. Перевірка стану: компенсувальна транзакція може виконатися успішно тільки в тому випадку, якщо пряма транзакція була реально застосована (або запис позначається як CANCELLED до виконання вихідного запиту, блокуючи його виконання).

4. Як змінюється мережевий latency при виборі між хореографією та оркестрацією для ланцюжка з 4 кроків?

Відповідь:

  • У хореографії повідомлення йдуть за ланцюжком: $A \to B \to C \to D$. Час виконання дорівнює: \(T_{total} = T_{A} + RTT(A \to B) + T_{B} + RTT(B \to C) + T_{C} + RTT(C \to D) + T_{D}\)
  • В оркестрації кожен крок вимагає обміну з координатором: $O \to A \to O \to B \to O \to C \to O \to D \to O$. Мережевих пересилань (RTT) стає вдвічі більше: \(RTT_{total} = 2 \times N \times RTT\) Проте оркестрація дозволяє виконувати незалежні кроки паралельно (наприклад, одночасно запросити перевірку антифроду та резервування товарів), що в хореографії вимагає створення складних вузлів злиття подій (Fan-Out / Fan-In).

🎯 Шпаргалка для інтерв’ю

Головне:

  • Хореографія: децентралізована, event-driven, кожен сервіс слухає події сусідів і вирішує, що робити. Немає єдиної точки відмови, але при ускладненні перетворюється на заплутаний «клубок спагеті» подій.
  • Оркестрація: централізована, command-driven, спеціальний координатор (SEC) явно каже кожному сервісу, що робити, спираючись на стейт-машину і Saga Log. Легко моніторити й тестувати, але оркестратор вимагає масштабування та захисту від збоїв.

Емпіричне правило вибору:

  • До 3 сервісів з лінійним флоу $\to$ Хореографія.
  • 4+ сервісів, розгалуження, паралельні кроки, суворі тайм-аути, необхідність візуального моніторингу бізнес-процесу $\to$ Оркестрація (Temporal, Camunda, Zeebe).
  • Різні домени/Bounded Contexts $\to$ Оркестрація всередині, хореографія ззовні.

Червоні прапорці (Чого говорити НЕ МОЖНА):

  • ❌ «Оркестратор — це сервіс із try-catch навколо REST-викликів» (Синхронний REST-оркестратор без Saga Log впаде при перезавантаженні та втратить транзакцію).
  • ❌ «У хореографії сервіси повністю ізольовані й не знають один про одного» (Сервіси жорстко пов’язані семантикою чужих подій та їхніми версіями).
  • ❌ «Оркестрація завжди краща за хореографію» (Оркестрація додає накладні витрати на мережеві hop’и та ризик створення God Object).

Пов’язані теми: