# Співбесіда Spring Boot: Поширення Транзакцій > Опануйте поширення транзакцій у Spring Boot: REQUIRED, REQUIRES_NEW, NESTED тощо. 12 питань зі співбесід з кодом та поширеними пастками. - Published: 2026-03-18 - Updated: 2026-05-01 - Author: SharpSkill - Tags: spring boot, transactions, propagation, java, interview - Reading time: 14 min --- Поширення транзакцій є фундаментальною концепцією у Spring Boot, яку регулярно перевіряють під час технічних співбесід. Розуміння того, як транзакції взаємодіють між методами з анотацією `@Transactional`, допомагає уникати тонких помилок у продакшені та проєктувати надійні архітектури. > **Порада щодо підготовки** > > Інтерв'юери оцінюють здатність обирати правильний рівень поширення відповідно до бізнес-контексту. Уміння пояснити, чому REQUIRES_NEW замість REQUIRED у конкретному випадку, робить різницю. ## Основи поширення транзакцій ### Питання 1: Що таке поширення транзакцій у Spring? Поширення визначає поведінку транзакційного методу, коли його викликають у контексті існуючої транзакції. Воно відповідає на питання: «Що відбувається, коли метод `@Transactional` викликає інший метод, який також має анотацію?» ```java // OrderService.java // Демонстрація концепції поширення @Service public class OrderService { private final PaymentService paymentService; private final OrderRepository orderRepository; // Батьківська транзакція - починає нову транзакцію @Transactional public void createOrder(OrderRequest request) { // Зберігає замовлення в поточній транзакції Order order = orderRepository.save(new Order(request)); // Виклик іншого методу @Transactional // Поширення визначає: та сама транзакція чи нова? paymentService.processPayment(order.getId(), request.getAmount()); } } @Service public class PaymentService { // Поширення за замовчуванням: REQUIRED // Приєднується до існуючої транзакції з createOrder() @Transactional public void processPayment(Long orderId, BigDecimal amount) { // Виконується в ТІЙ САМІЙ транзакції, що й createOrder() // Якщо цей метод зазнає невдачі, замовлення також буде відкочено } } ``` Spring надає сім рівнів поширення, кожен з яких підходить для специфічних бізнес-потреб. Вибір безпосередньо впливає на узгодженість даних і продуктивність. ### Питання 2: Опишіть поведінку REQUIRED (поширення за замовчуванням) REQUIRED — це поширення за замовчуванням. Якщо транзакція існує, метод приєднується до неї. Інакше створюється нова. Це найпоширеніша та найінтуїтивніша поведінка. ```java // UserService.java // REQUIRED: поведінка за замовчуванням @Service public class UserService { private final UserRepository userRepository; private final AuditService auditService; @Transactional(propagation = Propagation.REQUIRED) public void updateUser(Long userId, UserUpdateRequest request) { // Починає транзакцію, якщо жодної не існує User user = userRepository.findById(userId).orElseThrow(); user.setEmail(request.getEmail()); userRepository.save(user); // Аудит приєднується до тієї самої транзакції auditService.logUpdate(userId, "EMAIL_CHANGED"); } } @Service public class AuditService { private final AuditLogRepository auditLogRepository; @Transactional(propagation = Propagation.REQUIRED) public void logUpdate(Long userId, String action) { // Приєднується до транзакції updateUser() // Commit або rollback разом auditLogRepository.save(new AuditLog(userId, action, Instant.now())); } } ``` Діаграма нижче ілюструє транзакційний потік: ```text updateUser() починає TX-1 ├── save(user) → TX-1 └── logUpdate() → приєднується до TX-1 (REQUIRED) └── save(audit) → TX-1 Якщо logUpdate() зазнає невдачі → rollback TX-1 → user І audit скасовано ``` ### Питання 3: Коли використовувати REQUIRES_NEW замість REQUIRED? REQUIRES_NEW призупиняє існуючу транзакцію та створює нову незалежну транзакцію. Корисно, коли операція повинна бути зафіксована незалежно від результату батьківської транзакції. ```java // PaymentService.java // REQUIRES_NEW: незалежна транзакція @Service public class PaymentService { private final PaymentRepository paymentRepository; private final PaymentAuditService auditService; @Transactional public void processPayment(Long orderId, BigDecimal amount) { Payment payment = new Payment(orderId, amount); paymentRepository.save(payment); // Аудит МАЄ бути збережений, навіть якщо платіж пізніше зазнає невдачі auditService.logPaymentAttempt(orderId, amount); // Симуляція помилки після аудиту if (amount.compareTo(BigDecimal.ZERO) < 0) { throw new InvalidAmountException("Negative amount"); } } } @Service public class PaymentAuditService { private final PaymentAuditRepository auditRepository; // REQUIRES_NEW: фіксує незалежно від батьківської транзакції @Transactional(propagation = Propagation.REQUIRES_NEW) public void logPaymentAttempt(Long orderId, BigDecimal amount) { // Створено нову транзакцію TX-2 // TX-1 (processPayment) призупинено auditRepository.save(new PaymentAudit(orderId, amount, "ATTEMPTED")); // TX-2 фіксується тут, незалежно від TX-1 } } ``` Транзакційний потік з REQUIRES_NEW: ```text processPayment() починає TX-1 ├── save(payment) → TX-1 ├── logPaymentAttempt() → TX-1 ПРИЗУПИНЕНО │ └── починає TX-2 → нова транзакція │ └── save(audit) → TX-2 │ └── COMMIT TX-2 → audit збережено │ └── TX-1 ПРОДОВЖУЄ └── throw InvalidAmountException └── ROLLBACK TX-1 → платіж скасовано, але audit збережено ``` > **Стережіться deadlock'ів** > > REQUIRES_NEW може спричинити deadlock'и, якщо нова транзакція звертається до тих самих ресурсів, заблокованих призупиненою транзакцією. Уникайте використання REQUIRES_NEW для модифікації тих самих таблиць, що й батьківська транзакція. ### Питання 4: Поясніть поширення NESTED і чим воно відрізняється від REQUIRES_NEW NESTED створює savepoint у поточній транзакції. Якщо метод зазнає невдачі, відкочуються лише зміни з моменту savepoint, а не вся батьківська транзакція. ```java // BatchProcessingService.java // NESTED: savepoint у батьківській транзакції @Service public class BatchProcessingService { private final ItemRepository itemRepository; private final ItemProcessor itemProcessor; @Transactional public BatchResult processBatch(List items) { BatchResult result = new BatchResult(); for (Item item : items) { try { // Кожен елемент обробляється з savepoint itemProcessor.processItem(item); result.addSuccess(item.getId()); } catch (ProcessingException e) { // Rollback лише цього елемента, не всього batch result.addFailure(item.getId(), e.getMessage()); } } return result; // Commit успішних елементів } } @Service public class ItemProcessor { private final ItemRepository itemRepository; // NESTED: створює savepoint, частковий rollback можливий @Transactional(propagation = Propagation.NESTED) public void processItem(Item item) { item.setStatus("PROCESSING"); itemRepository.save(item); // Бізнес-валідація if (!isValid(item)) { throw new ProcessingException("Invalid item"); // Rollback до savepoint → лише цей елемент } item.setStatus("COMPLETED"); itemRepository.save(item); } } ``` Порівняння NESTED та REQUIRES_NEW: ```text NESTED: ├── Використовує savepoint у батьківській TX ├── При помилці → rollback до savepoint ├── Якщо батьківська TX робить rollback → NESTED також відкочується └── Більш продуктивний (без нового з'єднання) REQUIRES_NEW: ├── Створює повністю незалежну транзакцію ├── При помилці → rollback лише дочірньої TX ├── Якщо батьківська TX робить rollback → дочірня TX ВЖЕ ЗАФІКСОВАНА └── Потребує нового з'єднання ``` > **Підтримка NESTED** > > Поширення NESTED потребує підтримки JDBC savepoints. Більшість сучасних баз даних (PostgreSQL, MySQL, Oracle) підтримує це. Перевірте сумісність перед використанням. ## Розширені типи поширення ### Питання 5: Коли використовувати SUPPORTS і NOT_SUPPORTED? SUPPORTS виконується в існуючій транзакції, якщо вона є, інакше без транзакції. NOT_SUPPORTED призупиняє будь-яку існуючу транзакцію та виконується без транзакції. ```java // ReportingService.java // SUPPORTS: опціональна транзакція @Service public class ReportingService { private final ReportRepository reportRepository; // SUPPORTS: працює з транзакцією або без неї // Корисно для читань, які не потребують транзакційних гарантій @Transactional(propagation = Propagation.SUPPORTS) public Report generateReport(Long reportId) { // Якщо викликається з методу @Transactional → використовує його TX // Якщо викликається безпосередньо → без транзакції (читання OK) return reportRepository.generateComplexReport(reportId); } } @Service public class ExternalNotificationService { private final ExternalApiClient apiClient; // NOT_SUPPORTED: ніколи в транзакції // Уникає блокування TX під час повільного зовнішнього виклику @Transactional(propagation = Propagation.NOT_SUPPORTED) public void sendExternalNotification(String message) { // Батьківська TX призупинена під час виклику apiClient.send(message); // Потенційно повільний HTTP-виклик // Батьківська TX продовжує після } } ``` ```java // OrderService.java // Приклад комбінованого використання @Service public class OrderService { private final OrderRepository orderRepository; private final ReportingService reportingService; private final ExternalNotificationService notificationService; @Transactional public void completeOrder(Long orderId) { Order order = orderRepository.findById(orderId).orElseThrow(); order.setStatus("COMPLETED"); orderRepository.save(order); // Генерація звіту в тій самій TX (SUPPORTS) Report report = reportingService.generateReport(orderId); // Зовнішнє сповіщення ПОЗА транзакцією (NOT_SUPPORTED) // Уникає таймауту TX, якщо зовнішній API повільний notificationService.sendExternalNotification( "Order " + orderId + " completed" ); } } ``` ### Питання 6: Поясніть MANDATORY і NEVER MANDATORY вимагає існуючої транзакції та інакше викидає виняток. NEVER вимагає відсутності транзакції та викидає виняток, якщо вона існує. ```java // AuditService.java // MANDATORY: має викликатися з транзакції @Service public class AuditService { private final AuditRepository auditRepository; // MANDATORY: відмовляється виконуватися без існуючої транзакції // Гарантує, що аудит завжди атомарний з аудитованою операцією @Transactional(propagation = Propagation.MANDATORY) public void logCriticalAction(String action, Long entityId) { // Якщо викликається без транзакції → IllegalTransactionStateException auditRepository.save(new AuditLog(action, entityId, Instant.now())); } } // CacheService.java // NEVER: ніколи не повинен бути в транзакції @Service public class CacheService { private final CacheManager cacheManager; // NEVER: кеш не повинен брати участь у транзакціях // Уникає невідповідностей між кешем і БД після rollback @Transactional(propagation = Propagation.NEVER) public void invalidateCache(String cacheKey) { // Якщо викликається з @Transactional → IllegalTransactionStateException cacheManager.getCache("entities").evict(cacheKey); } } ``` ```java // SecurityService.java // Правильне використання MANDATORY @Service public class SecurityService { private final AuditService auditService; @Transactional public void changeUserPassword(Long userId, String newPassword) { // Чутлива операція... updatePassword(userId, newPassword); // Аудит МАЄ бути в тій самій транзакції // MANDATORY забезпечує це архітектурне обмеження auditService.logCriticalAction("PASSWORD_CHANGE", userId); } // ПОМИЛКА: прямий виклик без транзакції public void badUsage() { // Викидає IllegalTransactionStateException, оскільки немає TX auditService.logCriticalAction("TEST", 1L); } } ``` ## Поширені пастки на співбесіді ### Питання 7: Чому @Transactional не працює з внутрішніми викликами? Одна з найпоширеніших пасток. Внутрішні виклики методів (self-invocation) обходять Spring proxy, відключаючи управління транзакціями. ```java // BrokenService.java // КЛАСИЧНА ПОМИЛКА: self-invocation @Service public class BrokenService { private final ItemRepository itemRepository; public void processItems(List itemIds) { for (Long id : itemIds) { // ПАСТКА: внутрішній виклик → обходить proxy // @Transactional на processItem() ІГНОРУЄТЬСЯ processItem(id); } } @Transactional public void processItem(Long itemId) { // Ця транзакція НІКОЛИ не створюється при внутрішніх викликах Item item = itemRepository.findById(itemId).orElseThrow(); item.setStatus("PROCESSED"); itemRepository.save(item); } } ``` Рішення для уникнення цієї пастки: ```java // Рішення 1: Self-injection @Service public class FixedServiceWithSelfInjection { private final ItemRepository itemRepository; @Lazy @Autowired private FixedServiceWithSelfInjection self; // Self-injection public void processItems(List itemIds) { for (Long id : itemIds) { // Виклик через proxy → @Transactional працює self.processItem(id); } } @Transactional public void processItem(Long itemId) { // Транзакція створена правильно Item item = itemRepository.findById(itemId).orElseThrow(); item.setStatus("PROCESSED"); itemRepository.save(item); } } // Рішення 2: Розділити на два сервіси (рекомендоване) @Service public class ItemOrchestrator { private final ItemProcessor processor; public void processItems(List itemIds) { for (Long id : itemIds) { // Виклик до іншого bean → proxy працює processor.processItem(id); } } } @Service public class ItemProcessor { private final ItemRepository itemRepository; @Transactional public void processItem(Long itemId) { // Транзакція керується правильно Item item = itemRepository.findById(itemId).orElseThrow(); item.setStatus("PROCESSED"); itemRepository.save(item); } } ``` ### Питання 8: Як обробляти винятки та rollback? За замовчуванням Spring робить rollback лише на RuntimeException та Error. Checked винятки НЕ викликають автоматичний rollback. ```java // TransactionRollbackDemo.java // Поведінка rollback залежно від типу винятку @Service public class TransactionRollbackDemo { private final OrderRepository orderRepository; // RuntimeException → автоматичний ROLLBACK @Transactional public void methodWithRuntimeException() { orderRepository.save(new Order()); throw new RuntimeException("Error"); // ROLLBACK } // Checked Exception → БЕЗ rollback за замовчуванням @Transactional public void methodWithCheckedException() throws IOException { orderRepository.save(new Order()); throw new IOException("File error"); // Все одно COMMIT! } // Примусити rollback на checked exception @Transactional(rollbackFor = IOException.class) public void methodWithRollbackFor() throws IOException { orderRepository.save(new Order()); throw new IOException("Error"); // ROLLBACK завдяки rollbackFor } // Виключити RuntimeException з rollback @Transactional(noRollbackFor = BusinessException.class) public void methodWithNoRollbackFor() { orderRepository.save(new Order()); throw new BusinessException("Warning"); // COMMIT попри виняток } } ``` Рекомендована конфігурація для бізнес-випадків: ```java // BaseTransactionalService.java // Узгоджена транзакційна конфігурація @Service public abstract class BaseTransactionalService { // Rollback на всіх винятках (checked та unchecked) @Transactional(rollbackFor = Exception.class) protected void executeInTransaction(Runnable operation) { operation.run(); } } // PaymentService.java @Service public class PaymentService extends BaseTransactionalService { private final PaymentRepository paymentRepository; @Transactional( rollbackFor = Exception.class, noRollbackFor = InsufficientFundsException.class ) public PaymentResult processPayment(PaymentRequest request) { Payment payment = new Payment(request); paymentRepository.save(payment); if (request.getAmount().compareTo(getBalance()) > 0) { // НЕ робить rollback - хочемо зберегти запис спроби throw new InsufficientFundsException("Insufficient balance"); } return new PaymentResult(payment.getId(), "SUCCESS"); } } ``` ### Питання 9: Як ізоляція транзакцій працює з поширенням? Ізоляція та поширення доповнюють одне одного. Ізоляція визначає видимість даних між паралельними транзакціями. ```java // IsolationDemo.java // Поєднання ізоляції + поширення @Service public class IsolationDemo { private final AccountRepository accountRepository; // READ_COMMITTED: бачить дані, зафіксовані іншими транзакціями @Transactional( isolation = Isolation.READ_COMMITTED, propagation = Propagation.REQUIRED ) public BigDecimal getAccountBalance(Long accountId) { // Може бачити різні значення при повторному читанні під час TX return accountRepository.findById(accountId) .map(Account::getBalance) .orElse(BigDecimal.ZERO); } // REPEATABLE_READ: гарантує те саме читання протягом транзакції @Transactional( isolation = Isolation.REPEATABLE_READ, propagation = Propagation.REQUIRED ) public void transferWithConsistentRead(Long fromId, Long toId, BigDecimal amount) { Account from = accountRepository.findById(fromId).orElseThrow(); Account to = accountRepository.findById(toId).orElseThrow(); // Навіть якщо інша транзакція модифікує 'from' тим часом, // ми завжди бачимо початкове значення (snapshot) from.debit(amount); to.credit(amount); accountRepository.save(from); accountRepository.save(to); } // SERIALIZABLE: максимальна ізоляція, без конкурентності @Transactional( isolation = Isolation.SERIALIZABLE, propagation = Propagation.REQUIRES_NEW ) public void criticalOperation(Long accountId) { // Блокує будь-яку іншу транзакцію на цих даних // Використовуйте економно - вплив на продуктивність Account account = accountRepository.findById(accountId).orElseThrow(); account.performCriticalUpdate(); accountRepository.save(account); } } ``` Зведена таблиця рівнів ізоляції: ```text | Ізоляція | Dirty Read | Non-Repeatable | Phantom | |------------------|------------|----------------|---------| | READ_UNCOMMITTED | Можливо | Можливо | Можливо | | READ_COMMITTED | Ні | Можливо | Можливо | | REPEATABLE_READ | Ні | Ні | Можливо | | SERIALIZABLE | Ні | Ні | Ні | ``` > **Вплив на продуктивність** > > Чим суворіша ізоляція, тим більше продуктивність може страждати від блокувань. SERIALIZABLE може спричинити значну конкуренцію в продакшені з високим трафіком. ## Розширені патерни ### Питання 10: Як реалізувати патерн «transactional outbox»? Патерн outbox гарантує узгодженість між модифікаціями бази даних та надсиланням повідомлень/подій, навіть у разі збою. ```java // OutboxService.java // Патерн Transactional Outbox @Service public class OutboxService { private final OutboxRepository outboxRepository; // Зберігає подію в тій самій транзакції, що й бізнес-сутність @Transactional(propagation = Propagation.MANDATORY) public void saveEvent(String aggregateType, Long aggregateId, String eventType, String payload) { OutboxEvent event = OutboxEvent.builder() .aggregateType(aggregateType) .aggregateId(aggregateId) .eventType(eventType) .payload(payload) .status("PENDING") .createdAt(Instant.now()) .build(); outboxRepository.save(event); } } @Service public class OrderService { private final OrderRepository orderRepository; private final OutboxService outboxService; @Transactional public Order createOrder(OrderRequest request) { // Створити замовлення Order order = new Order(request); orderRepository.save(order); // Подія outbox у ТІЙ САМІЙ транзакції (MANDATORY) // Якщо commit успішний → обидва збережено // Якщо rollback → жодного не збережено outboxService.saveEvent( "ORDER", order.getId(), "ORDER_CREATED", toJson(new OrderCreatedEvent(order)) ); return order; } } // OutboxPublisher.java // Окремий процес, що публікує події @Service public class OutboxPublisher { private final OutboxRepository outboxRepository; private final MessageBroker messageBroker; // Незалежна транзакція для кожної публікації @Scheduled(fixedDelay = 1000) @Transactional(propagation = Propagation.REQUIRES_NEW) public void publishPendingEvents() { List events = outboxRepository .findByStatusOrderByCreatedAt("PENDING"); for (OutboxEvent event : events) { try { messageBroker.publish(event.getEventType(), event.getPayload()); event.setStatus("PUBLISHED"); event.setPublishedAt(Instant.now()); } catch (Exception e) { event.setStatus("FAILED"); event.setError(e.getMessage()); } outboxRepository.save(event); } } } ``` ### Питання 11: Як тестувати різні поведінки поширення? Тести поширення вимагають особливої уваги для перевірки очікуваної транзакційної поведінки. ```java // TransactionPropagationTest.java // Тестування поведінок поширення @SpringBootTest @Transactional class TransactionPropagationTest { @Autowired private OrderService orderService; @Autowired private PaymentService paymentService; @Autowired private TestEntityManager entityManager; @Test void required_shouldShareTransaction() { // Given Order order = orderService.createOrder(new OrderRequest()); // When - платіж у тій самій транзакції (REQUIRED) paymentService.processPayment(order.getId(), BigDecimal.TEN); // Then - обидва видно перед commit entityManager.flush(); assertThat(entityManager.find(Order.class, order.getId())).isNotNull(); assertThat(entityManager.find(Payment.class, order.getId())).isNotNull(); } @Test void requiresNew_shouldCommitIndependently() { // Given Long orderId = null; try { // When - audit у REQUIRES_NEW orderId = orderService.createOrderWithAudit(new OrderRequest()); throw new RuntimeException("Simulated failure after audit"); } catch (RuntimeException e) { // Основна транзакція робить rollback } // Then - audit (REQUIRES_NEW) все ще зафіксовано assertThat(findAuditLog(orderId)).isNotNull(); // Але замовлення відкочено assertThat(findOrder(orderId)).isNull(); } @Test void mandatory_shouldThrowWithoutTransaction() { // Given - немає активної транзакції // When/Then - має викинути виняток assertThatThrownBy(() -> auditService.logCriticalAction("TEST", 1L)) .isInstanceOf(IllegalTransactionStateException.class) .hasMessageContaining("No existing transaction"); } } ``` ```java // PropagationIntegrationTest.java // Інтеграційний тест із реальним rollback та commit @SpringBootTest class PropagationIntegrationTest { @Autowired private OrderService orderService; @Autowired private OrderRepository orderRepository; @Autowired private AuditLogRepository auditLogRepository; @Test void nested_shouldRollbackOnlyNestedOnFailure() { // Given int initialCount = orderRepository.findAll().size(); // When - batch-обробка з NESTED BatchResult result = orderService.processBatchWithNested( List.of(validItem(), invalidItem(), validItem()) ); // Then - тільки валідні елементи зберігаються assertThat(result.getSuccessCount()).isEqualTo(2); assertThat(result.getFailureCount()).isEqualTo(1); assertThat(orderRepository.findAll().size()).isEqualTo(initialCount + 2); } } ``` ### Питання 12: Які найкращі практики конфігурації транзакцій? Узгоджена транзакційна конфігурація на рівні проєкту уникає сюрпризів і полегшує підтримку. ```java // TransactionConfig.java // Централізована конфігурація транзакцій @Configuration @EnableTransactionManagement public class TransactionConfig { // Таймаут за замовчуванням для всіх транзакцій @Bean public PlatformTransactionManager transactionManager(DataSource dataSource) { DataSourceTransactionManager tm = new DataSourceTransactionManager(dataSource); tm.setDefaultTimeout(30); // 30 секунд максимум на транзакцію return tm; } } // BaseService.java // Анотації за замовчуванням для сервісів @Service @Transactional( readOnly = true, // Лише читання за замовчуванням rollbackFor = Exception.class // Rollback на будь-якому винятку ) public abstract class BaseService { // Методи читання успадковують readOnly = true } // OrderService.java // Сервіс з узгодженою конфігурацією @Service public class OrderService extends BaseService { private final OrderRepository orderRepository; // Успадковує readOnly = true public Order findById(Long id) { return orderRepository.findById(id).orElseThrow(); } // Override для записів @Transactional(readOnly = false) public Order createOrder(OrderRequest request) { return orderRepository.save(new Order(request)); } // Явна конфігурація для критичних випадків @Transactional( readOnly = false, propagation = Propagation.REQUIRES_NEW, isolation = Isolation.SERIALIZABLE, timeout = 10 ) public void criticalOperation(Long orderId) { // Операція з максимальною ізоляцією та коротким таймаутом } } ``` Чек-лист найкращих практик: ```text Конфігурація транзакцій - Чек-лист ✅ readOnly = true за замовчуванням, явний override для записів ✅ rollbackFor = Exception.class для включення checked exceptions ✅ Відповідний таймаут залежно від типу операції ✅ Уникати внутрішніх викликів (@Transactional ігнорується) ✅ REQUIRES_NEW лише коли потрібен незалежний commit ✅ MANDATORY для гарантії транзакційного контексту ✅ Явні тести поведінок rollback ✅ Моніторити довготривалі транзакції ✅ Документувати нестандартні вибори поширення ``` ## Висновок Поширення транзакцій — це фундаментальна концепція, яку оцінюють на співбесідах Spring Boot. Ключові моменти для запам'ятовування: **Поширені типи поширення:** - ✅ REQUIRED (за замовчуванням): приєднується або створює транзакцію - ✅ REQUIRES_NEW: незалежна транзакція, окремий commit - ✅ NESTED: savepoint для часткового rollback - ✅ MANDATORY: вимагає існуючої транзакції **Пастки, яких слід уникати:** - ✅ Self-invocation: обходить proxy, @Transactional ігнорується - ✅ Checked винятки: без rollback за замовчуванням - ✅ REQUIRES_NEW на тих самих даних: ризик deadlock - ✅ Відсутній таймаут: транзакції заблоковані необмежено **Найкращі практики:** - ✅ readOnly = true за замовчуванням - ✅ rollbackFor = Exception.class систематично - ✅ Окремі сервіси для уникнення self-invocation - ✅ Явно тестувати поведінки rollback Опанування поширення транзакцій демонструє глибоке розуміння Spring і управління даними. Ці концепції є важливими для проєктування надійних додатків і успішного проходження технічних співбесід. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/uk/blog/spring-boot/spring-boot-transaction-propagation-interview