Питання 4 · Розділ 17

Що таке компенсувальні транзакції

не ROLLBACK, а нова транзакція, яка робить протилежну дію.

Мовні версії: English Russian Ukrainian

🟢 Junior Level

Компенсувальна транзакція — це дія, яке скасовує результат попередньої успішної транзакції в Saga.

Важливо: оригінальна транзакція вже закоммічена і видна іншим. Компенсація — це не ROLLBACK, а нова транзакція, яка робить протилежну дію.

У мікросервісах неможливо зробити ROLLBACK як у звичайній БД. Замість цього потрібно вручну скасувати кожен крок.

Saga:
1. ✅ Створити замовлення
2. ✅ Списати гроші
3. ❌ Зарезервувати товар (FAIL!)

Компенсувальні транзакції (у зворотному порядку):
2. ✅ Повернути гроші (компенсація кроку 2)
1. ✅ Скасувати замовлення (компенсація кроку 1)

🟡 Middle Level

Правила компенсувальних транзакцій

1. Виконуються у зворотному порядку:

Кроки: A → B → C → D (fail)
Компенсації: compensate(C) → compensate(B) → compensate(A)

2. Мають бути ідемпотентними:

public void refundPayment(String paymentId) {
    if (paymentRepository.isRefunded(paymentId)) {
        return;  // Уже повернуто — не робимо нічого
    }
    paymentRepository.refund(paymentId);
    paymentRepository.markRefunded(paymentId);
}

3. Можуть не бути точною протилежністю:

// Крок: "Зарезервувати товар"
// Компенсація: НЕ "зняти резерв", а "перевести в статус available"

// Крок: "Надіслати email"
// Компенсація: НЕ "видалити email", а "надіслати email про скасування"

Типові помилки

  1. Забута компенсація:
    Order → Payment → Inventory (fail)
    Компенсація: refund(Payment) ... забули cancel(Order)!
    Підсумок: замовлення створене, гроші повернуті, але замовлення не скасоване
    

🔴 Senior Level

Архітектурні Trade-offs

Semantic compensation vs technical undo:

Technical undo = DELETE з таблиці.
Semantic compensation = надсилання email клієнту «ваше замовлення скасовано» + нарахування бонусних балів.

Semantic краще для бізнесу, але складніше реалізувати

Edge Cases

1. Compensation failure:

// Якщо компенсація теж впала — retry
// Спрощений приклад — у production додайте exponential backoff і jitter.
// Простий loop retry може «задовбити» падаючий сервіс.
public void compensateWithRetry(SagaStep step, int maxRetries) {
    for (int i = 0; i < maxRetries; i++) {
        try {
            step.compensate();
            return;
        } catch (Exception e) {
            log.warn("Compensation retry {}", i, e);
        }
    }
    // Після усіх retry — manual intervention
    alertService.sendAlert("Compensation failed after retries", step);
}

2. Non-compensatable steps:

Деякі кроки не можна скасувати:
- Email надіслано — не можна "забрати"
- SMS надіслано — не можна скасувати
- Реальний товар вже shipped

Рішення: позначити як non-compensatable і alert для manual fix

Production Experience

Saga з компенсаціями:

public class OrderSaga {

    public void onPaymentFailed(PaymentFailedEvent event) {
        // Компенсації у зворотному порядку
        compensateInventory(event.orderId());
        compensateOrder(event.orderId());
    }

    private void compensateInventory(String orderId) {
        try {
            inventoryClient.releaseReservation(orderId);
        } catch (Exception e) {
            log.error("Inventory compensation failed", e);
            alertService.sendAlert("Inventory compensation failed", orderId);
            // Retry або manual intervention
        }
    }

    private void compensateOrder(String orderId) {
        try {
            orderClient.cancelOrder(orderId);
        } catch (Exception e) {
            log.error("Order compensation failed", e);
            alertService.sendAlert("Order compensation failed", orderId);
        }
    }
}

Best Practices

✅ Компенсації у зворотному порядку
✅ Ідемпотентність кожної компенсації
✅ Retry для компенсацій
✅ Alert при failure компенсації
✅ Логуйте кожен крок компенсації

❌ Не забувайте компенсації
❌ Не ігноруйте failure компенсацій
❌ Не сподівайтесь що компенсація завжди спрацює

🎯 Шпаргалка для співбесіди

Обов’язково знати:

  • Компенсувальна транзакція — це НЕ ROLLBACK, а нова транзакція з протилежною дією
  • Компенсації виконуються у зворотному порядку (C→B→A)
  • Кожна компенсація має бути ідемпотентною
  • Semantic compensation краща за technical undo (email про скасування vs DELETE)
  • Деякі кроки non-compensatable (email надіслано, товар shipped)
  • При failure компенсації — retry + alert для manual intervention
  • Компенсація може не бути точною протилежністю кроку

Часті уточнюючі питання:

  • Що якщо компенсація теж впала? Retry з exponential backoff, потім alert для manual fix.
  • Які кроки не можна скасувати? Email/SMS надсилання, реальне відвантаження товару — позначити як non-compensatable.
  • Навіщо ідемпотентність компенсацій? Компенсація може викликатись двічі (retry, duplicate event).
  • Semantic vs technical compensation? Technical = DELETE з таблиці, semantic = нарахувати бонуси + сповістити клієнта.

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

  • “Компенсація = ROLLBACK” — ні, оригінальна транзакція вже закоммічена
  • “Компенсація завжди можлива” — ні, email не можна “забрати”
  • “Можна компенсувати в будь-якому порядку” — ні, строго у зворотному
  • “Ідемпотентність не важлива для компенсацій” — критично важлива

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

  • [[1. Що таке патерн Saga і коли його використовувати]]
  • [[2. У чому різниця між хореографією та оркестрацією в Saga]]
  • [[3. Як реалізувати розподілені транзакції в мікросервісах]]
  • [[19. Що таке патерн Retry і як його правильно використовувати]]
  • [[17. Як забезпечити відмовостійкість мікросервісів]]