Що таке Propagation в Spring
Spring реалізує propagation через AOP Proxy та TransactionInterceptor.
🟢 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 створює обгортку навколо біна, яка перехоплює виклики методів і додає cross-cutting логіку (транзакції, логування).
Виклик: 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);
// Якщо виняток → 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 стався, АЛЕ:
// 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:
auditService.logPayment()використовував REQUIRED — був частиною загальної транзакції- При
gateway.charge()exception → вся транзакція rollback, включаючи аудит - 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 TransactionMonitoring {
private final MeterRegistry registry;
public void trackSuspensions() {
// Monitor suspended transaction count
Gauge.builder("spring.transaction.suspended",
() -> getActiveSuspendedCount())
.register(registry);
}
}
Best Practices для Highload
- REQUIRED — ваш дефолт для бізнес-логіки — 90% випадків.
- REQUIRES_NEW тільки для аудиту/логування — не для пов’язаних даних.
- NESTED для batch processing — коли потрібен partial failure tolerance.
- Уникайте глибокого nesting — кожен REQUIRES_NEW = +1 connection.
- Monitor propagation usage — track через Micrometer.
- Self-invocation bypass — завжди викликайте через інший бин.
- Tune connection pool — якщо використовуєте REQUIRES_NEW, збільште pool size.
- Test rollback propagation — integration tests для кожного сценарію.
- Use timeout для REQUIRES_NEW — запобігає зависанню.
- Consider programmatic transactions — для складної умовної логіки.
🎯 Шпаргалка для інтерв’ю
Обов’язково знати:
- Propagation визначає, як методи з @Transactional взаємодіють при вкладених викликах
- 7 типів: REQUIRED (default), REQUIRES_NEW, NESTED, MANDATORY, SUPPORTS, NOT_SUPPORTED, NEVER
- REQUIRED = join існуючої або створення нової; REQUIRES_NEW = suspend + нова транзакція
- NESTED = savepoint всередині існуючої транзакції (тільки JDBC savepoints)
- Propagation працює тільки через Spring Proxy — self-invocation (this.method()) bypass-ить проксі
- REQUIRES_NEW overhead: suspend/resume ~5-15ms, +1 connection per call
Часті уточнюючі запитання:
- Чому inner REQUIRED метод кидає виняток, а outer ловить — все одно rollback? — Inner метод помічає транзакцію rollback-only
- Чим NESTED відрізняється від REQUIRES_NEW? — NESTED = savepoint (один commit), REQUIRES_NEW = окрема транзакція (окремий commit)
- Коли використовувати REQUIRES_NEW? — Аудит, логування, notification — дані мають зберегтися незалежно
- Чи працює propagation з reactive transactions? — Обмежено, через Reactor context propagation
Червоні прапорці (НЕ говорити):
- “this.method() працює з propagation” — self-invocation bypass-ить проксі
- “NESTED працює з JTA” — JTA не підтримує savepoints, fallback до REQUIRES_NEW
- “REQUIRES_NEW не впливає на connection pool” — кожен REQUIRES_NEW = +1 connection
Пов’язані теми:
- [[14. Що робить Propagation.NESTED]]
- [[15. В чому різниця між REQUIRED та REQUIRES_NEW]]
- [[16. Що таке анотація @Transactional]]
- [[22. Що станеться при виклику @Transactional методу з іншого методу того ж класу]]