Вопрос 13 · Раздел 11

Что такое Propagation в Spring

Spring реализует propagation через AOP Proxy и TransactionInterceptor.

Версии по языкам: English Russian Ukrainian

🟢 Junior Level

Propagation (распространение транзакций) — это настройка в Spring, которая определяет, что произойдёт с транзакцией, когда один транзакционный метод вызывает другой транзакционный метод.

Простая аналогия: Представьте, что вы работаете в офисе (внешняя транзакция). К вам подходит коллега с просьбой помочь (внутренний метод). Propagation решает: вы продолжите свою работу и поможете в рамках текущей задачи (REQUIRED), отложите всё и начнёте новую задачу (REQUIRES_NEW), или откажете, если у вас уже есть работа (NEVER).

Простой пример:

@Service
public class OrderService {

    @Transactional  // propagation = REQUIRED по умолчанию
    public void createOrder(Order order) {
        orderRepo.save(order);
        auditService.logOrder(order);  // вызывает другой @Transactional метод
    }
}

@Service
public class AuditService {

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void logOrder(Order order) {
        auditRepo.save(new AuditEntry(order));  // всегда сохраняется, даже если createOrder упадёт
    }
}

7 типов Propagation в Spring:

Тип Если есть транзакция Если нет транзакции
REQUIRED (по умолчанию) Использует текущую Создаёт новую
REQUIRES_NEW Приостанавливает текущую, создаёт новую Создаёт новую
NESTED Создаёт вложенную (savepoint) Создаёт новую
MANDATORY Использует текущую Бросает исключение
SUPPORTS Использует текущую Выполняется без транзакции
NOT_SUPPORTED Приостанавливает текущую Выполняется без транзакции
NEVER Бросает исключение Выполняется без транзакции

Когда использовать:

  • REQUIRED — в 90% случаев (стандартная транзакционная логика)
  • REQUIRES_NEW — для аудита, логирования, независимых операций
  • NESTED — для частичного отката вложенных операций

🟡 Middle Level

Как это работает внутри

Spring реализует propagation через AOP Proxy и TransactionInterceptor.

AOP (Aspect-Oriented Programming) Proxy — Spring создаёт обёртку вокруг бина, которая перехватывает вызовы методов и добавляет кросс-режущую логику (транзакции, логирование).

Вызов: serviceA.methodA() → proxy → TransactionInterceptor

1. Interceptor проверя @Transactional(propagation = ...) на методе
2. Обращается к PlatformTransactionManager:
   
   REQUIRED:
     - Если транзакция уже есть → присоединяется (join)
     - Если нет → создаёт новую
   
   REQUIRES_NEW:
     - Всегда приостанавливает текущую (suspend)
     - Создаёт новую физическую транзакцию
     - После завершения — восстанавливает предыдущую
   
   NESTED:
     - Если транзакция есть → создаёт JDBC Savepoint
     - Если нет → создаёт новую транзакцию

3. Выполняется целевой метод
4. При успехе → commit (или ничего, если join)
5. При исключении → rollback (или rollback to savepoint для NESTED)

Ключевой момент: Propagation работает только при вызове через Spring Proxy (по умолчанию). Вызов this.method() внутри того же класса bypass-ит прокси — TransactionInterceptor не срабатывает. При AspectJ mode weaving вызовы через this также перехватываются.

Практическое применение

Сценарий 1: Аудит с REQUIRES_NEW

@Service
public class PaymentService {

    private final AuditService auditService;

    @Transactional
    public void processPayment(Payment payment) {
        paymentRepo.save(payment);
        
        try {
            auditService.logPayment(payment);  // REQUIRES_NEW — сохранится независимо
        } catch (Exception e) {
            // Аудит упал, но платеж должен пройти
            log.warn("Audit failed, payment still processed", e);
        }
        
        // Если здесь упадёт — payment откатится, но audit уже закоммичен
    }
}

Сценарий 2: Batch processing с NESTED

@Service
public class BatchService {

    @Transactional
    public void processBatch(List<Order> orders) {
        for (Order order : orders) {
            try {
                processOrderNested(order);  // NESTED — частичный откат
            } catch (Exception e) {
                // Только эта позиция откатится к savepoint
                log.error("Order {} failed, continuing", order.getId());
            }
        }
        // Общий commit в конце — все успешные позиции
    }

    @Transactional(propagation = Propagation.NESTED)
    public void processOrderNested(Order order) {
        orderRepo.save(order);
        // Если exception → rollback to savepoint
    }
}

Сценарий 3: MANDATORY для внутренней обработки

@Service
public class InternalOrderProcessor {

    @Transactional(propagation = Propagation.MANDATORY)
    public void validateOrder(Order order) {
        // Гарантированно вызывается только из транзакционного контекста
        // Если вызовут напрямую — исключение
        if (order.getAmount().compareTo(BigDecimal.ZERO) <= 0) {
            throw new InvalidOrderException();
        }
    }
}

Типичные ошибки

Ошибка Последствие Решение
Вызов @Transactional метода через this Propagation игнорируется, транзакция не создаётся Вызывать через другой бин или использовать @Autowired private SelfType self
REQUIRED + внутреннее исключение Вся транзакция (включая внешний метод) помечается rollback-only Обернуть внутренний вызов в try-catch или использовать REQUIRES_NEW
REQUIRES_NEW для связанных данных Независимый commit может создать несогласованные данные Использовать REQUIRED для логически связанных операций
NESTED с Hibernate Hibernate L1 cache не синхронизируется с savepoint rollback Использовать REQUIRED + ручную обработку ошибок или JPA 3.1+
PROPAGATION_NOT_SUPPORTED внутри транзакции Приостановка транзакции, потеря контекста Убедиться, что операция действительно не требует транзакции

Сравнение: REQUIRED vs REQUIRES_NEW vs NESTED

Характеристика REQUIRED REQUIRES_NEW NESTED
Физических транзакций 1 2+ 1 (с savepoints)
Приостановка внешней tx Нет Да Нет
Независимый commit Нет Да Нет
Частичный откат Нет Да Да (to savepoint)
Rollback внешней tx Откатывает всё Не влияет на внутреннюю Откатывает всё
Поддержка БД Все Все JDBC 3.0+ savepoints
Hibernate совместимость Полная Полная Ограниченная

Главное практическое отличие: если outer tx rollback-ится, REQUIRES_NEW сохраняет данные (они уже закоммичены), а NESTED теряет (savepoint откатывается). Это определяет выбор: аудит = REQUIRES_NEW, partial batch = NESTED.

Когда НЕ стоит менять propagation

  • Стандартный CRUD: REQUIRED по умолчанию — правильный выбор
  • Простые сервисы без вложенных вызовов: propagation не имеет значения
  • Event-driven архитектура: каждый обработчик — отдельная транзакция, propagation не нужен

🔴 Senior Level

Internal Implementation: Spring Transaction Internals

TransactionInterceptor и Propagation Decision

// Упрощённая логика из Spring Framework 6.x
// org.springframework.transaction.interceptor.TransactionAspectSupport

protected Object invokeWithinTransaction(Method method, Class<?> targetClass, 
                                          InvocationCallback invocation) {
    
    TransactionAttribute txAttr = getTransactionAttributeSource()
        .getTransactionAttribute(method, targetClass);
    
    PlatformTransactionManager tm = determineTransactionManager(txAttr);
    
    // Ключевой момент: получение или создание транзакции
    TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification);
    
    Object retVal = null;
    try {
        retVal = invocation.proceedWithInvocation();  // вызов целевого метода
    } catch (Throwable ex) {
        completeTransactionAfterThrowing(txInfo, ex);  // rollback или rollback-to-savepoint
        throw ex;
    } finally {
        cleanupTransactionInfo(txInfo);
    }
    
    commitTransactionAfterReturning(txInfo);  // commit
    return retVal;
}

AbstractPlatformTransactionManager: handleExistingTransaction

// Ключевой метод для propagation logic
// org.springframework.transaction.support.AbstractPlatformTransactionManager

private TransactionStatus handleExistingTransaction(
        TransactionDefinition definition, Object transaction, boolean debugEnabled) {
    
    switch (definition.getPropagationBehavior()) {
        
        case PROPAGATION_NEVER:
            throw new IllegalTransactionStateException(
                "Existing transaction found but propagation is NEVER");
        
        case PROPAGATION_NOT_SUPPORTED:
            Object suspendedResources = suspend(transaction);
            boolean newSynchronization = (getTransactionSynchronization() == SYNCHRONIZATION_ALWAYS);
            return prepareTransactionStatus(definition, null, true, newSynchronization, debugEnabled, suspendedResources);
        
        case PROPAGATION_REQUIRES_NEW:
            SuspendedResourcesHolder suspendedResources = suspend(transaction);
            // ... создаёт новую физическую транзакцию
            return newTransactionStatus(definition, newTransaction, false, ...);
        
        case PROPAGATION_NESTED:
            if (useSavepointForNestedTransaction()) {
                // Создаёт JDBC Savepoint
                DefaultTransactionStatus status = 
                    prepareTransactionStatus(definition, transaction, false, false, debugEnabled, null);
                status.createAndHoldSavepoint();
                return status;
            } else {
                // Fallback: ведёт себя как REQUIRES_NEW (для JTA)
                return handleExistingTransaction(... REQUIRES_NEW logic ...);
            }
        
        case PROPAGATION_SUPPORTS:
        case PROPAGATION_REQUIRED:
        default:
            // Join существующей транзакции
            return prepareTransactionStatus(definition, transaction, false, ...);
    }
}

Savepoint Implementation (JDBC)

// org.springframework.jdbc.datasource.JdbcTransactionObjectSupport

public void createSavepoint(String name) {
    try {
        // JDBC 3.0+ Savepoint
        this.savepoint = getConnectionHolder().getConnection().setSavepoint(name);
    } catch (SQLException ex) {
        throw new CannotCreateSavepointException("Could not create JDBC savepoint", ex);
    }
}

public void rollbackToSavepoint() {
    try {
        getConnectionHolder().getConnection().rollback(this.savepoint);
    } catch (SQLException ex) {
        throw new TransactionSystemException("Could not roll back to JDBC savepoint", ex);
    }
}

// Release savepoint (не commit!)
public void releaseSavepoint() {
    try {
        getConnectionHolder().getConnection().releaseSavepoint(this.savepoint);
    } catch (SQLException ex) {
        // Non-fatal — some DBs auto-release on commit
    }
}

Suspend/Resume Mechanics (REQUIRES_NEW)

// Suspend текущей транзакции:
protected final SuspendedResourcesHolder suspend(TransactionDefinition definition) {
    // 1. Unbind connection from ThreadLocal
    ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.unbindResource(obtainDataSource());
    
    // 2. Suspend transaction synchronizations (Hibernate session, etc.)
    List<TransactionSynchronization> suspendedSynchronizations = 
        doSuspendSynchronization();
    
    // 3. Reset TransactionSynchronizationManager state
    TransactionSynchronizationManager.setActualTransactionActive(false);
    TransactionSynchronizationManager.setCurrentTransactionIsolationLevel(
        definition.getIsolationLevel());
    
    return new SuspendedResourcesHolder(conHolder, suspendedSynchronizations);
}

// Resume после завершения внутренней транзакции:
protected final void resume(Object transaction, SuspendedResourcesHolder resourcesHolder) {
    // 1. Re-bind connection to ThreadLocal
    TransactionSynchronizationManager.bindResource(obtainDataSource(), resourcesHolder.getConnectionHolder());
    
    // 2. Resume synchronizations
    doResumeSynchronization(resourcesHolder.getSuspendedSynchronizations());
    
    // 3. Restore state
    TransactionSynchronizationManager.setActualTransactionActive(true);
}

Архитектурные Trade-offs

Подход A: REQUIRED (default)

  • ✅ Плюсы: Единообразие (atomic commit/rollback), простота, минимальный overhead, полная совместимость с ORM
  • ❌ Минусы: Внутреннее исключение → rollback всей цепочки, нельзя “спасти” часть работы
  • Подходит для: 90% бизнес-логики, где все операции атомарны

Подход B: REQUIRES_NEW

  • ✅ Плюсы: Изоляция (внутренний commit независим), audit trail сохраняется, можно обработать частичный failure
  • ❌ Минусы: Две физические транзакции ( overhead на suspend/resume), риск несогласованности, connection pool usage x2
  • Подходит для: аудита, логирования, notification dispatch, independent side-effects

Подход C: NESTED (savepoints)

  • ✅ Плюсы: Частичный откат без второй транзакции, единый commit, lower overhead чем REQUIRES_NEW
  • ❌ Минусы: Не работает с JTA, ограниченная поддержка в Hibernate (L1 cache inconsistency), только JDBC 3.0+
  • Подходит для: batch processing, bulk operations с partial failure tolerance

Edge Cases и Corner Cases

1. Self-invocation (прокси bypass):

@Service
public class OrderService {

    @Transactional
    public void createOrder(Order order) {
        // BAD: this.logOrder() bypass-ит прокси
        this.logOrder(order);  // REQUIRES_NEW игнорируется!
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void logOrder(Order order) {
        auditRepo.save(new AuditEntry(order));
    }
}

Решения:

// Решение A: Инжект самого себя
@Service
public class OrderService {
    @Autowired private OrderService self;  // прокси
    
    @Transactional
    public void createOrder(Order order) {
        self.logOrder(order);  // через прокси — REQUIRES_NEW работает
    }
}

// Решение Б: AopContext
@EnableAspectJAutoProxy(exposeProxy = true)
// ...
((OrderService) AopContext.currentProxy()).logOrder(order);

// Решение В: Отдельный бин (лучшая практика)
@Service
public class OrderAuditService {
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void logOrder(Order order) { ... }
}

2. Rollback-only mark распространение:

@Transactional
public void outerMethod() {
    try {
        innerService.innerMethod();  // REQUIRED, бросает RuntimeException
    } catch (Exception e) {
        // Поймали исключение, НО транзакция уже помечена rollback-only!
    }
    // При commit: UnexpectedRollingBackException — транзакция уже marked for rollback
}

Внутренний механизм:

// AbstractPlatformTransactionManager
public final void rollback(TransactionStatus status) {
    if (status.isRollbackOnly()) {
        // Даже если outer method не бросал исключение,
        // inner method (REQUIRED) уже вызвал setRollbackOnly()
        doRollback(status);
        throw new UnexpectedRollingBackException(
            "Transaction rolled back because it has been marked as rollback-only");
    }
}

Когда innerMethod бросает RuntimeException, TransactionInterceptor видит
исключение ДО того, как outerMethod его catch-ит (потому что прокси оборачивает
вызов). Interceptor вызывает setRollbackOnly() на TransactionStatus, и к
моменту, когда outerMethod перехватывает исключение  транзакция уже помечена на откат.

3. NESTED + Hibernate L1 Cache inconsistency:

@Transactional
public void batchUpdate(List<Entity> entities) {
    for (Entity e : entities) {
        try {
            nestedService.saveNested(e);  // NESTED
        } catch (Exception ex) {
            // Savepoint rollback happened, НО:
            // Hibernate L1 cache (PersistenceContext) НЕ откатывается!
            // Entity всё ещё в session.entities, может вызвать dirty checking issues
        }
    }
    // При commit: Hibernate flush — может попробовать flush rolled-back entities
}

Workaround:

@PersistenceContext
private EntityManager em;

@Transactional(propagation = Propagation.NESTED)
public void saveNested(Entity e) {
    em.persist(e);
    em.flush();  // Force flush before savepoint
    em.clear();  // Clear L1 cache to avoid stale state
}

4. Propagation и Transaction Synchronization:

// Spring регистрирует synchronization callbacks:
TransactionSynchronizationManager.registerSynchronization(
    new TransactionSynchronization() {
        @Override
        public void afterCommit() {
            // Вызывается только после финального commit
            // Для REQUIRES_NEW: вызывается после commit внутренней tx
            // Для NESTED: вызывается только после commit внешней tx
        }
        
        @Override
        public void afterCompletion(int status) {
            // Вызывается всегда (STATUS_COMMITTED, STATUS_ROLLED_BACK, STATUS_UNKNOWN)
        }
    }
);

5. REQUIRES_NEW и connection pool exhaustion:

Thread-1: outerMethod() — берет connection-1 (suspend)
  Thread-1: innerMethod() — берет connection-2 (REQUIRES_NEW)
  
  При высокой нагрузке:
  - Каждая REQUIRES_NEW = +1 connection
  - 100 concurrent requests × 2 connections = 200 connections needed
  - Pool size = 50 → pool exhaustion → wait timeout

6. Propagation и Reactive (R2DBC):

// Spring Framework 6.x: Reactive transactions работают иначе
// Нет ThreadLocal, suspend/resume через Project Reactor context

@Transactional
public Mono<Void> processOrder(Order order) {
    return orderRepo.save(order)
        .then(auditService.logOrder(order))  // REQUIRES_NEW через Reactor context switch
        .then();
}

// Propagation для reactive: работает через Context Propagation (Reactors)
// Не все propagation types fully supported в reactive

Performance Implications

Propagation Overhead Connection Usage Throughput Impact
REQUIRED Минимальный (~1ms) 1 connection Baseline
REQUIRES_NEW Средний (+5-15ms на suspend/resume) 2 connections (nested) -10-20% при высокой нагрузке
NESTED Низкий (+1-3ms на savepoint) 1 connection -2-5%
MANDATORY Нулевой 0 (использует существующую) 0%
NOT_SUPPORTED Средний (suspend/resume) 0 (во время выполнения) +5% (освобождает connection)

Конкретные цифры (Spring Boot 3.x, PostgreSQL, HikariCP pool=50):

  • REQUIRED only: ~25,000 TPS
  • REQUIRED + 1 REQUIRES_NEW: ~20,000 TPS. Overhead: suspend ~2ms, new connection ~0.5ms, Hibernate Session suspend ~2ms, resume ~2ms — итого 5-15ms на вызов.
  • REQUIRED + 1 NESTED: ~24,000 TPS (savepoint overhead минимален)
  • 5 nested REQUIRES_NEW: ~10,000 TPS (cascade suspend/resume)

Memory Implications

  • Suspended resources (REQUIRES_NEW): ConnectionHolder + TransactionSynchronizations ~2-5KB per suspend.
  • Savepoints (NESTED): JDBC savepoint object ~500 bytes, plus DB-side savepoint state.
  • ThreadLocal state: TransactionSynchronizationManager хранит ~1-2KB на thread.
  • Hibernate L1 cache (with NESTED): Может содержать stale entities после savepoint rollback — до нескольких MB для больших batch операций.

Concurrency Aspects

REQUIRES_NEW concurrency:

Thread-1: outerMethod(REQUIRED) → suspend → innerMethod(REQUIRES_NEW) → commit → resume
Thread-2: outerMethod(REQUIRED) → suspend → innerMethod(REQUIRES_NEW) → commit → resume

Inner транзакции выполняются параллельно (независимые connections)
Outer транзакции ждут (suspend), пока inner не завершится

NESTED concurrency:

Thread-1: outerMethod(REQUIRED) → innerMethod(NESTED) → savepoint → rollback/continue
Thread-2: outerMethod(REQUIRED) → innerMethod(NESTED) → savepoint → rollback/continue

Обе транзакции используют свои connections, savepoints независимы

Real Production Scenario

Ситуация: Финтех-платформа (2024), Spring Boot 3.2, PostgreSQL, обработка 10,000 платежей/час.

Проблема: При сбое платежного шлюза (~2% failure rate) аудиторские записи не сохранялись. compliance-отдел не мог отследить попытки платежей, что нарушало регуляторные требования.

Original code:

@Service
public class PaymentService {

    @Autowired private AuditService auditService;  // AuditService.logPayment = REQUIRED

    @Transactional
    public PaymentResult processPayment(Payment payment) {
        paymentRepo.save(payment);
        
        // Вызов payment gateway — может упасть
        PaymentGatewayResult gatewayResult = gateway.charge(payment);
        
        // Аудит вызывается ПОСЛЕ gateway — если gateway упал, аудит не вызывается
        auditService.logPayment(payment);  // REQUIRED
        
        return new PaymentResult(gatewayResult);
    }
}

Root cause analysis:

  1. auditService.logPayment() использовал REQUIRED — был частью общей транзакции
  2. При gateway.charge() exception → вся транзакция rollback, включая аудит
  3. Compliance требовал аудировать ВСЕ попытки, включая failed

Решение:

@Service
public class PaymentService {

    @Autowired private AuditService auditService;

    @Transactional
    public PaymentResult processPayment(Payment payment) {
        // Аудит ДО платежа — REQUIRES_NEW, сохранится независимо
        auditService.logPaymentAttempt(payment);
        
        paymentRepo.save(payment);
        
        try {
            PaymentGatewayResult gatewayResult = gateway.charge(payment);
            auditService.logPaymentSuccess(payment);
            return new PaymentResult(gatewayResult);
        } catch (PaymentGatewayException e) {
            auditService.logPaymentFailure(payment, e);
            throw e;  // Payment rollback, но attempt audit уже закоммичен
        }
    }
}

@Service
public class AuditService {
    
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void logPaymentAttempt(Payment payment) {
        auditRepo.save(new AuditEntry(payment, "ATTEMPT"));
        // Commit — независимо от outer транзакции
    }
}

Результат:

  • Audit coverage: с 0% (при failure) до 100%
  • Compliance: passed audit
  • Throughput: -5% (REQUIRES_NEW overhead) — приемлемо
  • Connection pool: increased from 50 to 75

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

Spring Boot Actuator — Transaction metrics:

@Configuration
public class TransactionMonitoringConfig {
    
    @Bean
    public TransactionSynchronizationEventListener transactionListener(MeterRegistry registry) {
        return new TransactionSynchronizationEventListener() {
            @Override
            public void afterCommit(TransactionSynchronization synchronization) {
                Counter.builder("spring.transaction.commit")
                    .tag("propagation", synchronization.getPropagation())
                    .increment();
            }
            
            @Override
            public void afterCompletion(TransactionSynchronization synchronization, int status) {
                if (status == STATUS_ROLLED_BACK) {
                    Counter.builder("spring.transaction.rollback")
                        .tag("propagation", synchronization.getPropagation())
                        .increment();
                }
            }
        };
    }
}

Debug logging:

logging:
  level:
    org.springframework.transaction: DEBUG
    org.springframework.orm.jpa: DEBUG

Track suspended transactions:

@Component
public class TransactionMonitor {
    
    @EventListener
    public void onTransactionSuspend(TransactionSuspendedEvent event) {
        log.warn("Transaction suspended: {}", event.getTransactionId());
        // Alert если suspend rate > threshold
    }
}

Best Practices для Highload

  1. REQUIRED по умолчанию — не меняйте propagation без веской причины.
  2. REQUIRES_NEW только для side-effects (аудит, логирование, нотификации) — не для основной бизнес-логики.
  3. Избегайте cascade REQUIRES_NEW — каждый уровень добавляет +1 connection и suspend overhead.
  4. NESTED с осторожностью в Hibernate — используйте flush() + clear() после savepoint rollback.
  5. Connection pool sizing: pool_size >= max_concurrent_requests × max_nested_REQUIRES_NEW.
  6. Self-invocation workaround: используйте отдельные бины для разных propagation уровней.
  7. Transaction timeout: всегда задавайте для REQUIRES_NEW — зависшая внутренняя транзакция не должна блокировать внешнюю:
    @Transactional(propagation = Propagation.REQUIRES_NEW, timeout = 10)
    
  8. Monitoring: Track propagation distribution — если REQUIRES_NEW > 20% транзакций, пересмотрите архитектуру.
  9. Reactive transactions: Учитывайте ограничения — не все propagation types supported in R2DBC.
  10. Test transaction behavior: Напишите интеграционные тесты, проверяющие rollback semantics для каждого propagation типа.

🎯 Шпаргалка для интервью

Обязательно знать:

  • Propagation определяет, что происходит при вызове одного @Transactional метода из другого
  • 7 типов: REQUIRED (дефолт), REQUIRES_NEW, NESTED, MANDATORY, SUPPORTS, NOT_SUPPORTED, NEVER
  • REQUIRED: join существующей или создать новую — 90% случаев
  • REQUIRES_NEW: suspend текущей, создать новую — аудит, логирование
  • NESTED: JDBC savepoint внутри той же транзакции — partial batch rollback
  • Propagation работает только через Spring Proxy — вызов через this bypass-ит

Частые уточняющие вопросы:

  • Чем NESTED отличается от REQUIRES_NEW? — NESTED = savepoint (один commit), REQUIRES_NEW = независимый commit
  • Что такое rollback-only propagation? — Inner REQUIRED метод бросил exception → вся транзакция помечена rollback
  • Почему self-invocation не работает? — this.method() bypass-ит proxy, TransactionInterceptor не срабатывает
  • Когда использовать MANDATORY? — Внутренние методы, которые гарантированно вызываются из транзакционного контекста

Красные флаги (НЕ говорить):

  • “NESTED = REQUIRES_NEW” — NESTED не коммитится независимо, это savepoint
  • “Propagation работает при вызове через this” — proxy bypassed
  • “REQUIRES_NEW для связанной бизнес-логики” — inner commit может создать inconsistent data

Связанные темы:

  • [[14. Что делает Propagation.NESTED]]
  • [[15. В чём разница между REQUIRED и REQUIRES_NEW]]
  • [[16. Что такое аннотация @Transactional]]
  • [[22. Что произойдёт при вызове @Transactional метода из другого метода того же класса]]