Питання 15 · Розділ 11

В чому різниця між REQUIRED та REQUIRES_NEW

4. Завжди задавайте timeout для REQUIRES_NEW: @Transactional(propagation = REQUIRES_NEW, timeout = 5). 5. Уникайте cascade REQUIRES_NEW — кожен рівень подвоює connection usage....

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

🟢 Junior Level

REQUIRED та REQUIRES_NEW — це два найпопулярніші типи поширення транзакцій в Spring. Головна відмінність: REQUIRED використовує вже існуючу транзакцію, а REQUIRES_NEW завжди створює нову.

Проста аналогія:

  • REQUIRED — як робота над одним документом з колегою. Ви обидва редагуєте один файл. Якщо хтось один вирішить все видалити — втратять обидва.
  • REQUIRES_NEW — як два окремі документи. Кожен працює у своєму файлі. Якщо один зіпсує свій документ — інший не постраждає.

SQL-приклад:

// REQUIRED (за замовчуванням)
@Transactional  // propagation = REQUIRED
public void createOrder(Order order) {
    orderRepo.save(order);
    auditService.log(order);  // REQUIRED → той самий commit
    // Якщо createOrder впаде — І order, І audit відкочаться
}

// REQUIRES_NEW
@Transactional
public void createOrder(Order order) {
    orderRepo.save(order);
    auditService.log(order);  // REQUIRES_NEW → незалежний commit
    // Якщо createOrder впаде — order відкотиться, АЛЕ audit вже збережений
}

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

Характеристика REQUIRED REQUIRES_NEW
Нова транзакція Тільки якщо немає поточної Завжди
Призупинка поточної Ні Так
Commit Один загальний в кінці Незалежний
Rollback внутрішньої Відкочує всю tx Тільки внутрішню
Rollback зовнішньої Відкочує внутрішню НЕ впливає на внутрішню

Коли використовувати REQUIRED:

  • Стандартна бізнес-логіка (90% випадків)
  • Коли всі операції мають бути атомарними

Коли використовувати REQUIRES_NEW:

  • Аудит, логування
  • Сповіщення, які мають зберегтися незалежно
  • Операції, які не повинні залежати від зовнішньої транзакції

🟡 Middle Level

Як це працює всередині

REQUIRED:

Виклик: serviceA.methodA() → proxy → TransactionInterceptor

1. Interceptor перевіряє: чи є активна транзакція?
   - Так → приєднується (join), increment reference count
   - Ні → створює нову через TransactionManager

2. Виконується метод
3. При commit:
   - Якщо це outer tx → commit
   - Якщо inner tx (join) → нічого не робить (commit тільки в outer)
4. При rollback:
   - setRollbackOnly() → помічає всю транзакцію для відкату
   - Outer tx при commit бачить rollback-only → UnexpectedRollingBackException

REQUIRES_NEW:

Виклик: serviceA.methodA() → proxy → innerService.methodB(REQUIRES_NEW)

1. Suspend поточної транзакції:
   - Connection unbind from ThreadLocal
   - Transaction synchronizations suspended
   - TransactionSynchronizationManager state reset

2. Створюється нова фізична транзакція:
   - Новий connection з pool
   - Новий BEGIN в БД

3. Виконується метод
4. При commit → commit внутрішньої транзакції
5. При rollback → rollback внутрішньої транзакції
6. Resume зовнішньої транзакції:
   - Connection re-bind to ThreadLocal
   - Synchronizations resumed

Практичне застосування

Сценарій 1: REQUIRED для атомарної бізнес-логіки

@Service
public class OrderService {

    @Autowired private PaymentService paymentService;
    @Autowired private InventoryService inventoryService;

    @Transactional  // REQUIRED
    public OrderResult placeOrder(Order order) {
        // Всі операції мають бути атомарними
        inventoryService.reserve(order.getItems());    // REQUIRED — join
        PaymentResult payment = paymentService.charge(order.getTotal());  // REQUIRED — join

        if (!payment.isSuccess()) {
            throw new PaymentFailedException();
            // Rollback: І reserve, І charge відкочаться — консистентність
        }

        orderRepo.save(order);
        return OrderResult.success();
    }
}

Сценарій 2: REQUIRES_NEW для аудиту

@Service
public class PaymentService {

    @Autowired private AuditService auditService;

    @Transactional  // REQUIRED
    public PaymentResult processPayment(Payment payment) {
        // Аудит ДО платежу — має зберегтися навіть при збої
        auditService.logPaymentAttempt(payment);  // REQUIRES_NEW

        try {
            PaymentResult result = gateway.charge(payment);
            auditService.logPaymentSuccess(payment);  // REQUIRES_NEW
            return result;
        } catch (GatewayException e) {
            auditService.logPaymentFailure(payment, e);  // REQUIRES_NEW
            throw e;
            // Payment відкотиться, але attempt audit вже закомічений
        }
    }
}

@Service
public class AuditService {

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void logPaymentAttempt(Payment payment) {
        auditRepo.save(new AuditEntry(payment, "ATTEMPT"));
        // Commit — незалежно від зовнішньої транзакції
    }
}

Сценарій 3: REQUIRES_NEW для незалежних side-effects

@Service
public class NotificationService {

    @Transactional(propagation = Propagation.REQUIRES_NEW, timeout = 5)
    public void sendOrderConfirmation(Order order) {
        // Якщо відправка email впала — замовлення все одно має зберегтися
        emailService.send(order.getEmail(), "Order Confirmed");
        notificationRepo.save(new Notification(order, "EMAIL_SENT"));
        // Commit — notification збережений навіть якщо outer tx rollback
    }
}

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

Помилка Наслідок Рішення
REQUIRED + внутрішній виняток Вся транзакція marked rollback-only try-catch всередині inner метода або REQUIRES_NEW
REQUIRES_NEW для пов’язаних даних Inner commit створює неузгоджені дані — ЯКЩО внутрішня транзакція записує дані, логічно залежні від зовнішньої (наприклад, order_items без order). Для незалежних даних (аудит) — це коректна поведінка. Використовувати REQUIRED для логічно пов’язаних операцій
REQUIRES_NEW без timeout Зависла inner tx блокує outer tx Завжди задавати timeout для REQUIRES_NEW
Припущення, що REQUIRES_NEW = атомарність з outer Inner tx коммітиться одразу, outer може відкотитися Розуміти: REQUIRES_NEW = незалежна tx
REQUIRED при виклику через this Propagation ігнорується Використовувати окремий бін або self-injection

Порівняння: REQUIRED vs REQUIRES_NEW

Характеристика REQUIRED REQUIRES_NEW
Фізичних транзакцій 1 2+
Suspend зовнішньої tx Ні Так
Connection usage 1 2 (suspend + new)
Independent commit Ні Так
Rollback propagation На весь ланцюжок Тільки своя tx
Outer rollback effect Відкочує inner Не впливає на inner
Throughput overhead Мінімальний +10-20%
Connection pool pressure Низьке Високе (x2 per call)
Deadlock risk Низький Середній (2 connections)
Use case Основна бізнес-логіка Аудит, логування, нотифікації

Коли НЕ варто використовувати REQUIRES_NEW

  • Атомарна бізнес-логіка — якщо всі операції мають commit/rollback разом
  • Високонавантажені системи — suspend/resume overhead + connection pool pressure
  • Мікросервіси з eventual consistency — кожен сервіс управляє своєю транзакцією
  • Коли inner tx залежить від outer tx даних — inner commit може закомітити incomplete data

🔴 Senior Level

Internal Implementation: Suspend/Resume та Transaction Synchronization

Suspend Mechanics (REQUIRES_NEW)

// AbstractPlatformTransactionManager.suspend()

protected final SuspendedResourcesHolder suspend(@Nullable Object transaction) {
    if (transaction != null) {
        // 1. Suspend transaction synchronizations (Hibernate session, etc.)
        List<TransactionSynchronization> suspendedSynchronizations =
            doSuspendSynchronization();

        // 2. Unbind connection from ThreadLocal
        // Spring зберігає JDBC-з'єднання в ThreadLocal через ConnectionHolder.
        // unbindResource = відв'язати connection від поточного потоку
        // bindResource = прив'язати назад
        ConnectionHolder conHolder = (ConnectionHolder)
            TransactionSynchronizationManager.unbindResource(obtainDataSource());

        // 3. Reset TransactionSynchronizationManager state
        boolean wasActive = TransactionSynchronizationManager.isActualTransactionActive();
        TransactionSynchronizationManager.setActualTransactionActive(false);
        IsolationLevel currentIsolation =
            TransactionSynchronizationManager.getCurrentTransactionIsolationLevel();
        TransactionSynchronizationManager.setCurrentTransactionIsolationLevel(null);
        TransactionSynchronizationManager.setReadOnly(false);
        TransactionSynchronizationManager.setCurrentTransactionName(null);

        return new SuspendedResourcesHolder(
            conHolder, suspendedSynchronizations, wasActive, currentIsolation);
    }
    return null;
}

Що suspends-иться:

// doSuspendSynchronization() — suspends all registered synchronizations

// TransactionSynchronization callbacks:
interface TransactionSynchronization {
    void suspend();    // Викликається при suspend
    void resume();     // Викликається при resume
    void beforeCommit(boolean readOnly);
    void beforeCompletion();
    void afterCommit();
    void afterCompletion(int status);
}

// Registered synchronizations включають:
// - Hibernate Session synchronization
// - JPA EntityManager synchronization
// - Custom synchronizations (registered by application code)

Resume Mechanics

// AbstractPlatformTransactionManager.resume()

protected final void resume(@Nullable Object transaction,
                             @Nullable SuspendedResourcesHolder resourcesHolder) {
    if (resourcesHolder != null) {
        Object suspendedResources = resourcesHolder.getSuspendedResources();

        if (suspendedResources != null) {
            // 1. Re-bind connection to ThreadLocal
            TransactionSynchronizationManager.bindResource(
                obtainDataSource(), resourcesHolder.getConnectionHolder());
        }

        // 2. Resume synchronizations
        doResumeSynchronization(resourcesHolder.getSuspendedSynchronizations());

        // 3. Restore state
        TransactionSynchronizationManager.setActualTransactionActive(
            resourcesHolder.wasActive());
        TransactionSynchronizationManager.setCurrentTransactionIsolationLevel(
            resourcesHolder.getSuspendedIsolationLevel());
    }
}

REQUIRED Join Logic

// handleExistingTransaction() — REQUIRED case

case PROPAGATION_REQUIRED:
    // Join існуючої транзакції
    // НОВА транзакція НЕ створюється
    // Reference count increment-иться

    return prepareTransactionStatus(
        definition,           // ті самі параметри
        transaction,          // той самий transaction object
        false,                // newTransaction = false
        false,                // newSynchronization = false
        debugEnabled,
        null);                // no suspended resources

// Важливо: rollback propagation
// Якщо inner метод (REQUIRED) кидає RuntimeException:
// 1. TransactionInterceptor ловить виняток
// 2. Викликає: status.setRollbackOnly()
// 3. AbstractPlatformTransactionManager помічає ВСЮ транзакцію
// 4. Outer метод не може це скасувати

Rollback-only flag propagation:

// AbstractPlatformTransactionManager

public final void rollback(TransactionStatus status) throws TransactionException {
    if (status.isCompleted()) {
        return;  // Вже завершена
    }

    DefaultTransactionStatus defStatus = (DefaultTransactionStatus) status;

    if (defStatus.isNewTransaction()) {
        // Це outer tx → робимо rollback
        doRollback(defStatus);
    } else {
        // Це inner tx (join) → тільки mark rollback-only
        defStatus.setRollbackOnly();
        // Outer tx при commit побачить цей flag і зробить rollback
    }
}

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

Підхід A: REQUIRED

  • ✅ Плюси: Atomicity (все або нічого), простота, мінімальний overhead (1 connection), повна ORM сумісність
  • ❌ Мінуси: Немає partial failure tolerance, inner exception → full rollback, rollback-only propagation
  • Підходить для: financial transactions, order processing, any operation requiring full atomicity

Підхід B: REQUIRES_NEW

  • ✅ Плюси: Independent commit, partial failure handling, audit trail, isolation from outer tx failures
  • ❌ Мінуси: Suspend/resume overhead (5-15ms), x2 connection usage, risk of data inconsistency (inner committed, outer rolled back)
  • Підходить для: audit logging, notification dispatch, independent side-effects, compliance tracking

Підхід C: REQUIRES_NEW + Compensation

@Transactional
public void processWithCompensation(Data data) {
    try {
        sideEffectService.doWork(data);  // REQUIRES_NEW — commit
    } catch (Exception e) {
        // sideEffect вже закомічений — потрібна компенсація
        compensationService.undoSideEffect(data);  // REQUIRES_NEW
        throw e;  // Outer tx rollback
    }
}
  • ✅ Плюси: Best of both worlds — independence + compensation
  • ❌ Мінуси: Складність, потрібно implement undo для кожного side-effect
  • Підходить для: saga patterns, distributed systems, eventual consistency

Edge Cases та Corner Cases

1. Rollback-only propagation (найчастіша проблема):

@Transactional
public void outerMethod() {
    try {
        innerService.innerMethod();  // REQUIRED
    } catch (Exception e) {
        // Зловили виняток...
    }
    // ...але транзакція вже marked rollback-only!
    // При commit: UnexpectedRollingBackException
}

@Transactional
public void innerMethod() {  // REQUIRED
    repo.save(entity);
    throw new RuntimeException("fail");
    // TransactionInterceptor: status.setRollbackOnly()
}

Чому так відбувається:

innerMethod() — REQUIRED, join існуючої транзакції
При винятку:
  → TransactionInterceptor.completeTransactionAfterThrowing()
  → tm.rollback(txInfo.getTransactionStatus())
  → AbstractPlatformTransactionManager.rollback()
  → isNewTransaction() = false (join)
  → defStatus.setRollbackOnly()  // НЕ doRollback!

Outer method при commit:
  → tm.commit(status)
  → status.isRollbackOnly() = true
  → doRollback(status)  // Forced rollback!
  → throw UnexpectedRollingBackException

Рішення:

// Рішення A: REQUIRES_NEW
innerService.innerMethod();  // REQUIRES_NEW — rollback тільки своєї tx

// Рішення Б: try-catch всередині inner метода
@Transactional
public void innerMethod() {
    try {
        repo.save(entity);
        throw new RuntimeException("fail");
    } catch (Exception e) {
        log.error("Inner failed, but won't rollback outer", e);
        // Виняток не propagates → outer tx не marked rollback-only
    }
}

// Рішення В: noRollbackFor
@Transactional(noRollbackFor = BusinessException.class)
public void innerMethod() {
    repo.save(entity);
    throw new BusinessException("fail");  // Не викликає rollback
}

2. REQUIRES_NEW та connection pool exhaustion:

Навантаження: 100 concurrent requests
Кожен request: 1 outer tx + 2 REQUIRES_NEW calls = 3 connections

Total connections needed: 100 × 3 = 300
HikariCP pool size: 50

Результат:
  - 50 requests отримують connections
  - 250 чекають connectionAcquisitionTimeout (за замовчуванням 30 сек)
  - Timeout → request failure

Рішення:

spring:
  datasource:
    hikari:
      maximum-pool-size: 150  # >= max_concurrent × max_nested_tx
      connection-timeout: 5000  # faster failure

3. REQUIRES_NEW та Hibernate Session:

@Transactional
public void outerMethod() {
    Entity outer = em.find(Entity.class, 1L);  // loaded in Session 1
    outer.setName("outer-change");

    innerService.innerMethod();  // REQUIRES_NEW → suspend Session 1, create Session 2

    // Session 2 НЕ бачить changes з Session 1 (не flushed)
    // Якщо innerMethod читає той самий entity → побачить старе значення

    // При resume Session 1:
    // Session 1 все ще містить "outer-change" в dirty state
    // Але в БД зміни ще не записані
}

Impact: Inner tx може read stale data. Рішення: em.flush() перед REQUIRES_NEW викликом.

4. Transaction Synchronization та REQUIRES_NEW:

// outer tx registers synchronization
TransactionSynchronizationManager.registerSynchronization(
    new TransactionSynchronization() {
        @Override
        public void afterCommit() {
            // Викликається після outer tx commit
        }

        @Override
        public void afterCompletion(int status) {
            // Викликається після outer tx completion
        }

        @Override
        public void suspend() {
            // Викликається при REQUIRES_NEW suspend
        }

        @Override
        public void resume() {
            // Викликається при REQUIRES_NEW resume
        }
    }
);

// inner tx (REQUIRES_NEW) має свої own synchronizations
// inner afterCommit викликається після inner commit, НЕ outer

5. REQUIRES_NEW та Isolation Level:

@Transactional
public void outerMethod() {
    // outer tx використовує default isolation (або задане на outer)

    innerService.innerMethod();  // REQUIRES_NEW
    // inner tx може задати свій isolation level
}

@Transactional(propagation = Propagation.REQUIRES_NEW,
               isolation = Isolation.SERIALIZABLE)
public void innerMethod() {
    // inner tx: SERIALIZABLE
    // outer tx: DEFAULT (Read Committed)
    // Ці isolation levels НЕ впливають один на одного (різні фізичні tx)
}

6. REQUIRES_NEW та Timeout:

@Transactional(propagation = Propagation.REQUIRES_NEW, timeout = 5)
public void innerMethod() {
    // timeout = 5 seconds для inner tx ТІЛЬКИ
    // outer tx timeout НЕ affected

    longRunningOperation();  // Якщо > 5 сек → inner tx timeout
    // outer tx продовжує працювати (але inner вже відкотився)
}

7. Nested REQUIRES_NEW cascade:

@Transactional
public void level0() {
    level1Service.level1();  // REQUIRES_NEW → suspend L0, create L1

    @Transactional(propagation = REQUIRES_NEW)
    public void level1() {
        level2Service.level2();  // REQUIRES_NEW → suspend L1, create L2
    }
}

// Connection stack:
// L0: connection-1 (suspended)
// L1: connection-2 (suspended)
// L2: connection-3 (active)
//
// Commit order: L2 → resume L1 → L1 commit → resume L0 → L0 commit
// Rollback order: L2 rollback → resume L1 → L1 може continue або rollback

Performance Implications

Метрика REQUIRED REQUIRES_NEW
Latency (overhead) ~0ms (baseline) +5-15ms (suspend/resume)
Connection usage 1 2+ (per nested call)
Throughput (single call) 25,000 TPS 20,000 TPS (-20%)
Throughput (5 nested) 25,000 TPS 10,000 TPS (-60%)
Connection pool pressure 1x Nx (N = nesting depth)
Hibernate Session sync overhead None Flush + clear per suspend/resume

Цифри наведені для ілюстрації відносних пропорцій (Spring Boot 3.2, PostgreSQL 15, HikariCP pool=50). Абсолютні значення залежать від БД, схеми, заліза та навантаження. Ключовий takeaway — REQUIRES_NEW додає 20-60% overhead на кожен рівень вкладеності.

Scenario: 1 outer tx + 1 REQUIRES_NEW call
  - Suspend: ~2ms
  - New connection acquisition: ~0.5ms
  - Inner tx execution: baseline
  - Inner commit: ~1ms
  - Resume: ~2ms
  - Total overhead: ~5.5ms per REQUIRES_NEW call
    Suspend включає: unbind Connection з ThreadLocal (~0.1ms), suspend Hibernate
    Session та flush L1 cache (~1.5ms), unregister transaction synchronizations (~0.4ms).

Scenario: 1 outer tx + 3 REQUIRES_NEW calls
  - Total overhead: ~16.5ms
  - Throughput reduction: ~35%

Connection pool sizing:
  - REQUIRED only: pool = max_concurrent_requests
  - With 1 REQUIRES_NEW: pool = max_concurrent_requests × 2
  - With N REQUIRES_NEW: pool = max_concurrent_requests × (N + 1)

Memory Implications

  • Suspended ConnectionHolder: ~2KB (connection reference + state)
  • Suspended TransactionSynchronizations: ~500 bytes – 5KB (залежить від кількості)
  • ThreadLocal state per suspend: ~1-2KB
  • Hibernate Session state (suspended): ~100KB – 5MB (залежить від L1 cache size)
  • Inner tx EntityManager: ~100KB – 5MB (новий PersistenceContext)

Total per REQUIRES_NEW call: ~200KB – 10MB (в основному Hibernate Session state)

Concurrency Aspects

REQUIRES_NEW concurrent execution:

Thread-1: outer(REQUIRED) → suspend → inner1(REQUIRES_NEW) → commit → resume → outer commit
Thread-2: outer(REQUIRED) → suspend → inner2(REQUIRES_NEW) → commit → resume → outer commit

inner1 та inner2 виконуються паралельно (різні connections)
outer1 та outer2 чекають на suspend phase, поки inner не завершиться

Deadlock potential:
  - Inner tx може заблокувати рядки, потрібні outer tx після resume
  - Outer tx може заблокувати рядки, потрібні inner tx
  → Deadlock (detectable by DB deadlock detector)

Deadlock scenario:

T1-outer: UPDATE accounts SET balance = 900 WHERE id = 1;  -- lock row 1
T1-inner(REQUIRES_NEW): UPDATE accounts SET balance = 800 WHERE id = 2;  -- lock row 2
T2-inner(REQUIRES_NEW): UPDATE accounts SET balance = 700 WHERE id = 1;  -- wait row 1
T2-outer: UPDATE accounts SET balance = 600 WHERE id = 2;  -- wait row 2

→ Deadlock! T1-inner wait T2-outer, T2-inner wait T1-outer

Timeline: T=0: T1-outer locks row 1. T=1: T2-inner tries row 1 — BLOCKED.
T=2: T1-inner locks row 2. T=3: T2-outer tries row 2 — BLOCKED.
Deadlock detector: T2-outer → T1-inner → T2-inner → T1-outer → cycle.

Real Production Scenario

Ситуація: Healthcare data platform (2024), Spring Boot 3.1, MySQL 8.0, обробка 5,000 patient records/hour.

Проблема: При обробці записів пацієнтів, audit log втрачався при збої обробки. Регуляторний аудит виявив, що 15% attempted accesses не були залогьовані.

Original code:

@Transactional
public void processPatientRecord(PatientRecord record) {
    // Audit з REQUIRED — частина загальної транзакції
    auditService.logAccess(record);  // REQUIRED

    // Обробка даних — може впасти
    Data validated = validateAndTransform(record);
    patientRepo.save(validated);

    // Якщо validateAndTransform впав → rollback ВСЬОГО, включаючи audit
    // Audit запис втрачається → compliance violation
}

Root cause: auditService.logAccess() використовував REQUIRED за замовчуванням. При будь-якому збої в обробці, audit запис відкочувався разом з основною транзакцією.

Рішення:

@Transactional
public void processPatientRecord(PatientRecord record) {
    // Аудит ДО обробки — REQUIRES_NEW
    auditService.logAccess(record);  // REQUIRES_NEW → commit одразу

    try {
        Data validated = validateAndTransform(record);
        patientRepo.save(validated);
        auditService.logAccessSuccess(record);  // REQUIRES_NEW
    } catch (ValidationException e) {
        auditService.logAccessFailure(record, e);  // REQUIRES_NEW → commit
        throw e;  // Outer rollback, але access log вже збережений
    }
}

@Service
public class AuditService {

    @Transactional(propagation = Propagation.REQUIRES_NEW, timeout = 3)
    public void logAccess(PatientRecord record) {
        auditRepo.save(new AuditEntry(record, "ACCESS"));
        // Commit — незалежно від outer tx
    }
}

Результат:

  • Audit coverage: з 85% до 100%
  • Compliance: passed audit
  • Throughput: -8% (REQUIRES_NEW overhead)
  • Connection pool: increased from 30 to 60
  • Mean latency: +6ms per request

Lessons learned:

  1. Всі audit-записи мають бути REQUIRES_NEW
  2. Connection pool потрібно розмірювати з урахуванням nested tx
  3. Timeout на REQUIRES_NEW критичний (завислий audit не повинен блокувати outer)

Monitoring та Діагностика

Track propagation distribution:

@Component
public class PropagationMonitor implements TransactionSynchronization {

    private final MeterRegistry registry;
    private final Counter requiredCounter;
    private final Counter requiresNewCounter;

    public PropagationMonitor(MeterRegistry registry) {
        this.requiredCounter = Counter.builder("spring.transaction.propagation")
            .tag("type", "REQUIRED").register(registry);
        this.requiresNewCounter = Counter.builder("spring.transaction.propagation")
            .tag("type", "REQUIRES_NEW").register(registry);
    }

    @Override
    public void afterCommit() {
        // Track based on transaction attributes
    }

    @Override
    public void afterCompletion(int status) {
        if (status == STATUS_ROLLED_BACK) {
            Counter.builder("spring.transaction.rollback")
                .tag("propagation", getCurrentPropagation())
                .increment();
        }
    }
}

Connection pool monitoring:

// HikariCP metrics
Gauge.builder("hikaricp.connections.active", hikariDataSource,
        ds -> ds.getHikariPoolMXBean().getActiveConnections())
    .register(registry);

// Alert: active_connections > pool_size * 0.8

Suspend/resume tracking:

@Component
public class SuspendMonitor implements TransactionSynchronization {

    private final Timer suspendResumeTimer;

    @Override
    public void suspend() {
        // Record suspend timestamp
        ThreadLocal<Long> suspendTime = new ThreadLocal<>();
        suspendTime.set(System.currentTimeMillis());
    }

    @Override
    public void resume() {
        Long start = suspendTime.get();
        if (start != null) {
            suspendResumeTimer.record(System.currentTimeMillis() - start,
                TimeUnit.MILLISECONDS);
        }
    }
}

Best Practices для Highload

  1. REQUIRED за замовчуванням — не змінюйте без обґрунтування.
  2. REQUIRES_NEW тільки для side-effects — audit, logging, notification.
  3. Розмірюйте connection pool: pool_size >= max_concurrent × (1 + max_REQUIRES_NEW_per_request).
  4. Завжди задавайте timeout для REQUIRES_NEW: @Transactional(propagation = REQUIRES_NEW, timeout = 5).
  5. Уникайте cascade REQUIRES_NEW — кожен рівень подвоює connection usage.
  6. Flush перед REQUIRES_NEW викликом якщо inner tx читає ті самі дані:
    em.flush();  // Перед innerService.method() — REQUIRES_NEW
    innerService.method();
    
  7. Monitor rollback-only propagation rate — якщо >5%, код має architectural issues.
  8. Test під навантаженням — connection pool exhaustion manifest-иться тільки при високому навантаженні.
  9. Consider async alternatives для audit:
    @Async
    public void logAsync(Event event) {
        // Non-blocking, no REQUIRES_NEW overhead
        auditRepo.save(new AuditEntry(event));
    }
    
  10. Document propagation decisions — кожен REQUIRES_NEW має бути задокументований з обґрунтуванням.

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

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

  • REQUIRED: join існуючої транзакції або створити нову — одна фізична tx, один commit
  • REQUIRES_NEW: suspend поточної, створити нову — дві незалежні tx, два commit
  • REQUIRED: inner exception → вся транзакція rollback-only; REQUIRES_NEW: inner rollback не впливає на outer
  • REQUIRES_NEW вимагає 2 connections (suspend + new), overhead 5-15ms на виклик
  • Rollback-only propagation: inner REQUIRED кинув RuntimeException → outer не може commit-ити
  • REQUIRES_NEW для side-effects (аудит, логування), REQUIRED для основної бізнес-логіки

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

  • Що таке UnexpectedRollbackException? — Outer намагається commit-ити, але inner вже виставив rollback-only
  • Як розмірити connection pool для REQUIRES_NEW? — pool_size >= max_concurrent × (1 + nested_REQUIRES_NEW)
  • Чому Hibernate Session може read stale data при REQUIRES_NEW? — Suspended Session 1 не бачить changes з Session 2
  • Коли REQUIRES_NEW небезпечний? — Коли inner tx записує дані, логічно залежні від outer

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

  • “REQUIRES_NEW = атомарність з outer” — inner commit-иться незалежно
  • “REQUIRED не впливає на продуктивність” — inner exception → full rollback
  • “REQUIRES_NEW без timeout — нормально” — зависла inner tx блокує outer

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

  • [[13. Що таке Propagation в Spring]]
  • [[14. Що робить Propagation.NESTED]]
  • [[18. Що таке rollback в транзакціях]]
  • [[22. Що станеться при виклику @Transactional методу з іншого методу того ж класу]]