# Rozmowa Spring Boot: Propagacja Transakcji > Opanuj propagację transakcji w Spring Boot: REQUIRED, REQUIRES_NEW, NESTED i więcej. 12 pytań rekrutacyjnych z kodem i typowymi pułapkami. - Published: 2026-03-18 - Updated: 2026-05-01 - Author: SharpSkill - Tags: spring boot, transactions, propagation, java, interview - Reading time: 14 min --- Propagacja transakcji stanowi fundamentalne pojęcie w Spring Boot, regularnie sprawdzane podczas rozmów technicznych. Zrozumienie, jak transakcje współdziałają między metodami z adnotacją `@Transactional`, pomaga unikać subtelnych błędów produkcyjnych i pozwala projektować solidne architektury. > **Wskazówka przygotowawcza** > > Rekruterzy oceniają zdolność wyboru właściwego poziomu propagacji w zależności od kontekstu biznesowego. Umiejętność wyjaśnienia, dlaczego REQUIRES_NEW zamiast REQUIRED w konkretnym przypadku, robi różnicę. ## Podstawy propagacji transakcji ### Pytanie 1: Czym jest propagacja transakcji w Spring? Propagacja definiuje zachowanie metody transakcyjnej, gdy jest wywoływana w kontekście istniejącej transakcji. Odpowiada na pytanie: „Co dzieje się, gdy metoda `@Transactional` wywołuje inną metodę również z adnotacją?" ```java // OrderService.java // Demonstracja koncepcji propagacji @Service public class OrderService { private final PaymentService paymentService; private final OrderRepository orderRepository; // Transakcja nadrzędna - rozpoczyna nową transakcję @Transactional public void createOrder(OrderRequest request) { // Zapisuje zamówienie w bieżącej transakcji Order order = orderRepository.save(new Order(request)); // Wywołanie innej metody @Transactional // Propagacja decyduje: ta sama transakcja czy nowa? paymentService.processPayment(order.getId(), request.getAmount()); } } @Service public class PaymentService { // Domyślna propagacja: REQUIRED // Dołącza do istniejącej transakcji z createOrder() @Transactional public void processPayment(Long orderId, BigDecimal amount) { // Wykonuje się w TEJ SAMEJ transakcji co createOrder() // Jeśli ta metoda zawiedzie, zamówienie również zostanie cofnięte } } ``` Spring zapewnia siedem poziomów propagacji, każdy dostosowany do konkretnych potrzeb biznesowych. Wybór wpływa bezpośrednio na spójność danych i wydajność. ### Pytanie 2: Opisz zachowanie REQUIRED (domyślna propagacja) REQUIRED to domyślna propagacja. Jeśli istnieje transakcja, metoda do niej dołącza. W przeciwnym razie tworzona jest nowa. To najczęstsze i najbardziej intuicyjne zachowanie. ```java // UserService.java // REQUIRED: domyślne zachowanie @Service public class UserService { private final UserRepository userRepository; private final AuditService auditService; @Transactional(propagation = Propagation.REQUIRED) public void updateUser(Long userId, UserUpdateRequest request) { // Rozpoczyna transakcję, jeśli żadna nie istnieje User user = userRepository.findById(userId).orElseThrow(); user.setEmail(request.getEmail()); userRepository.save(user); // Audyt dołącza do tej samej transakcji auditService.logUpdate(userId, "EMAIL_CHANGED"); } } @Service public class AuditService { private final AuditLogRepository auditLogRepository; @Transactional(propagation = Propagation.REQUIRED) public void logUpdate(Long userId, String action) { // Dołącza do transakcji updateUser() // Commit lub rollback razem auditLogRepository.save(new AuditLog(userId, action, Instant.now())); } } ``` Poniższy diagram ilustruje przepływ transakcyjny: ```text updateUser() rozpoczyna TX-1 ├── save(user) → TX-1 └── logUpdate() → dołącza do TX-1 (REQUIRED) └── save(audit) → TX-1 Jeśli logUpdate() zawiedzie → rollback TX-1 → user I audit anulowane ``` ### Pytanie 3: Kiedy używać REQUIRES_NEW zamiast REQUIRED? REQUIRES_NEW zawiesza istniejącą transakcję i tworzy nową niezależną transakcję. Przydatne, gdy operacja musi zostać zatwierdzona niezależnie od wyniku transakcji nadrzędnej. ```java // PaymentService.java // REQUIRES_NEW: niezależna transakcja @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); // Audyt MUSI być utrwalony nawet jeśli płatność później zawiedzie auditService.logPaymentAttempt(orderId, amount); // Symulacja błędu po audycie if (amount.compareTo(BigDecimal.ZERO) < 0) { throw new InvalidAmountException("Negative amount"); } } } @Service public class PaymentAuditService { private final PaymentAuditRepository auditRepository; // REQUIRES_NEW: zatwierdza niezależnie od transakcji nadrzędnej @Transactional(propagation = Propagation.REQUIRES_NEW) public void logPaymentAttempt(Long orderId, BigDecimal amount) { // Utworzona nowa transakcja TX-2 // TX-1 (processPayment) jest zawieszona auditRepository.save(new PaymentAudit(orderId, amount, "ATTEMPTED")); // TX-2 zatwierdza tutaj, niezależnie od TX-1 } } ``` Przepływ transakcyjny z REQUIRES_NEW: ```text processPayment() rozpoczyna TX-1 ├── save(payment) → TX-1 ├── logPaymentAttempt() → TX-1 ZAWIESZONA │ └── rozpoczyna TX-2 → nowa transakcja │ └── save(audit) → TX-2 │ └── COMMIT TX-2 → audit utrwalony │ └── TX-1 WZNAWIA └── throw InvalidAmountException └── ROLLBACK TX-1 → płatność anulowana, ale audit zachowany ``` > **Uważaj na deadlocki** > > REQUIRES_NEW może powodować deadlocki, jeśli nowa transakcja uzyskuje dostęp do tych samych zasobów zablokowanych przez zawieszoną transakcję. Unikaj używania REQUIRES_NEW do modyfikowania tych samych tabel co transakcja nadrzędna. ### Pytanie 4: Wyjaśnij propagację NESTED i czym różni się od REQUIRES_NEW NESTED tworzy savepoint w obrębie bieżącej transakcji. Jeśli metoda zawiedzie, cofane są tylko zmiany od savepointa, a nie cała transakcja nadrzędna. ```java // BatchProcessingService.java // NESTED: savepoint w transakcji nadrzędnej @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 { // Każdy element przetwarzany jest z savepointem itemProcessor.processItem(item); result.addSuccess(item.getId()); } catch (ProcessingException e) { // Rollback tylko tego elementu, nie całego batcha result.addFailure(item.getId(), e.getMessage()); } } return result; // Commit udanych elementów } } @Service public class ItemProcessor { private final ItemRepository itemRepository; // NESTED: tworzy savepoint, częściowy rollback możliwy @Transactional(propagation = Propagation.NESTED) public void processItem(Item item) { item.setStatus("PROCESSING"); itemRepository.save(item); // Walidacja biznesowa if (!isValid(item)) { throw new ProcessingException("Invalid item"); // Rollback do savepointa → tylko ten element } item.setStatus("COMPLETED"); itemRepository.save(item); } } ``` Porównanie NESTED vs REQUIRES_NEW: ```text NESTED: ├── Używa savepointa w TX nadrzędnej ├── W przypadku błędu → rollback do savepointa ├── Jeśli TX nadrzędna robi rollback → NESTED również się cofa └── Wydajniejsze (brak nowego połączenia) REQUIRES_NEW: ├── Tworzy całkowicie niezależną transakcję ├── W przypadku błędu → rollback tylko TX potomnej ├── Jeśli TX nadrzędna robi rollback → TX potomna JUŻ ZATWIERDZONA └── Wymaga nowego połączenia ``` > **Wsparcie NESTED** > > Propagacja NESTED wymaga wsparcia savepointów JDBC. Większość nowoczesnych baz danych (PostgreSQL, MySQL, Oracle) to wspiera. Sprawdź zgodność przed użyciem. ## Zaawansowane typy propagacji ### Pytanie 5: Kiedy używać SUPPORTS i NOT_SUPPORTED? SUPPORTS wykonuje się w istniejącej transakcji, jeśli istnieje, w przeciwnym razie bez transakcji. NOT_SUPPORTED zawiesza istniejącą transakcję i wykonuje się bez transakcji. ```java // ReportingService.java // SUPPORTS: opcjonalna transakcja @Service public class ReportingService { private final ReportRepository reportRepository; // SUPPORTS: działa z transakcją lub bez // Przydatne dla odczytów niewymagających gwarancji transakcyjnych @Transactional(propagation = Propagation.SUPPORTS) public Report generateReport(Long reportId) { // Jeśli wywoływana z metody @Transactional → używa jej TX // Jeśli wywoływana bezpośrednio → bez transakcji (odczyt OK) return reportRepository.generateComplexReport(reportId); } } @Service public class ExternalNotificationService { private final ExternalApiClient apiClient; // NOT_SUPPORTED: nigdy w transakcji // Unika blokowania TX podczas wolnego wywołania zewnętrznego @Transactional(propagation = Propagation.NOT_SUPPORTED) public void sendExternalNotification(String message) { // TX nadrzędna zawieszona podczas wywołania apiClient.send(message); // Potencjalnie wolne wywołanie HTTP // TX nadrzędna wznawia po } } ``` ```java // OrderService.java // Przykład łącznego użycia @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); // Generowanie raportu w tej samej TX (SUPPORTS) Report report = reportingService.generateReport(orderId); // Powiadomienie zewnętrzne POZA transakcją (NOT_SUPPORTED) // Unika timeoutu TX, jeśli zewnętrzne API jest wolne notificationService.sendExternalNotification( "Order " + orderId + " completed" ); } } ``` ### Pytanie 6: Wyjaśnij MANDATORY i NEVER MANDATORY wymaga istniejącej transakcji i rzuca wyjątek w przeciwnym razie. NEVER wymaga braku transakcji i rzuca wyjątek, jeśli istnieje. ```java // AuditService.java // MANDATORY: musi być wywołane z wnętrza transakcji @Service public class AuditService { private final AuditRepository auditRepository; // MANDATORY: odmawia wykonania bez istniejącej transakcji // Gwarantuje, że audyt jest zawsze atomowy z audytowaną operacją @Transactional(propagation = Propagation.MANDATORY) public void logCriticalAction(String action, Long entityId) { // Jeśli wywołane bez transakcji → IllegalTransactionStateException auditRepository.save(new AuditLog(action, entityId, Instant.now())); } } // CacheService.java // NEVER: nigdy nie może być w transakcji @Service public class CacheService { private final CacheManager cacheManager; // NEVER: cache nie powinien uczestniczyć w transakcjach // Unika niespójności między cache a DB po rollbacku @Transactional(propagation = Propagation.NEVER) public void invalidateCache(String cacheKey) { // Jeśli wywołane z @Transactional → IllegalTransactionStateException cacheManager.getCache("entities").evict(cacheKey); } } ``` ```java // SecurityService.java // Poprawne użycie MANDATORY @Service public class SecurityService { private final AuditService auditService; @Transactional public void changeUserPassword(Long userId, String newPassword) { // Wrażliwa operacja... updatePassword(userId, newPassword); // Audyt MUSI być w tej samej transakcji // MANDATORY wymusza to ograniczenie architektoniczne auditService.logCriticalAction("PASSWORD_CHANGE", userId); } // BŁĄD: bezpośrednie wywołanie bez transakcji public void badUsage() { // Rzuca IllegalTransactionStateException, ponieważ brak TX auditService.logCriticalAction("TEST", 1L); } } ``` ## Typowe pułapki rekrutacyjne ### Pytanie 7: Dlaczego @Transactional nie działa przy wywołaniach wewnętrznych? Jedna z najczęstszych pułapek. Wywołania wewnętrzne metod (self-invocation) omijają proxy Springa, wyłączając zarządzanie transakcjami. ```java // BrokenService.java // KLASYCZNY BŁĄD: self-invocation @Service public class BrokenService { private final ItemRepository itemRepository; public void processItems(List itemIds) { for (Long id : itemIds) { // PUŁAPKA: wywołanie wewnętrzne → omija proxy // @Transactional na processItem() jest IGNOROWANE processItem(id); } } @Transactional public void processItem(Long itemId) { // Ta transakcja NIGDY nie jest tworzona przy wywołaniach wewnętrznych Item item = itemRepository.findById(itemId).orElseThrow(); item.setStatus("PROCESSED"); itemRepository.save(item); } } ``` Rozwiązania, aby uniknąć tej pułapki: ```java // Rozwiązanie 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) { // Wywołanie przez proxy → @Transactional działa self.processItem(id); } } @Transactional public void processItem(Long itemId) { // Transakcja poprawnie utworzona Item item = itemRepository.findById(itemId).orElseThrow(); item.setStatus("PROCESSED"); itemRepository.save(item); } } // Rozwiązanie 2: Podział na dwa serwisy (zalecane) @Service public class ItemOrchestrator { private final ItemProcessor processor; public void processItems(List itemIds) { for (Long id : itemIds) { // Wywołanie do innego beana → proxy działa processor.processItem(id); } } } @Service public class ItemProcessor { private final ItemRepository itemRepository; @Transactional public void processItem(Long itemId) { // Transakcja poprawnie zarządzana Item item = itemRepository.findById(itemId).orElseThrow(); item.setStatus("PROCESSED"); itemRepository.save(item); } } ``` ### Pytanie 8: Jak obsługiwać wyjątki i rollback? Domyślnie Spring robi rollback tylko dla RuntimeException i Error. Wyjątki checked NIE wywołują automatycznego rollbacka. ```java // TransactionRollbackDemo.java // Zachowanie rollbacka w zależności od typu wyjątku @Service public class TransactionRollbackDemo { private final OrderRepository orderRepository; // RuntimeException → automatyczny ROLLBACK @Transactional public void methodWithRuntimeException() { orderRepository.save(new Order()); throw new RuntimeException("Error"); // ROLLBACK } // Checked Exception → BRAK rollbacka domyślnie @Transactional public void methodWithCheckedException() throws IOException { orderRepository.save(new Order()); throw new IOException("File error"); // I tak COMMIT! } // Wymuszenie rollbacka przy checked exception @Transactional(rollbackFor = IOException.class) public void methodWithRollbackFor() throws IOException { orderRepository.save(new Order()); throw new IOException("Error"); // ROLLBACK dzięki rollbackFor } // Wykluczenie RuntimeException z rollbacka @Transactional(noRollbackFor = BusinessException.class) public void methodWithNoRollbackFor() { orderRepository.save(new Order()); throw new BusinessException("Warning"); // COMMIT mimo wyjątku } } ``` Zalecana konfiguracja dla przypadków biznesowych: ```java // BaseTransactionalService.java // Spójna konfiguracja transakcyjna @Service public abstract class BaseTransactionalService { // Rollback dla wszystkich wyjątków (checked i 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) { // BRAK rollbacka - chcemy zachować zapis próby throw new InsufficientFundsException("Insufficient balance"); } return new PaymentResult(payment.getId(), "SUCCESS"); } } ``` ### Pytanie 9: Jak działa izolacja transakcji z propagacją? Izolacja i propagacja są komplementarne. Izolacja określa widoczność danych między równoległymi transakcjami. ```java // IsolationDemo.java // Połączenie izolacji + propagacji @Service public class IsolationDemo { private final AccountRepository accountRepository; // READ_COMMITTED: widzi dane zatwierdzone przez inne transakcje @Transactional( isolation = Isolation.READ_COMMITTED, propagation = Propagation.REQUIRED ) public BigDecimal getAccountBalance(Long accountId) { // Może zobaczyć różne wartości przy ponownym odczycie podczas TX return accountRepository.findById(accountId) .map(Account::getBalance) .orElse(BigDecimal.ZERO); } // REPEATABLE_READ: gwarantuje ten sam odczyt podczas transakcji @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(); // Nawet jeśli inna transakcja zmodyfikuje 'from' w międzyczasie, // zawsze widzimy wartość początkową (snapshot) from.debit(amount); to.credit(amount); accountRepository.save(from); accountRepository.save(to); } // SERIALIZABLE: maksymalna izolacja, brak współbieżności @Transactional( isolation = Isolation.SERIALIZABLE, propagation = Propagation.REQUIRES_NEW ) public void criticalOperation(Long accountId) { // Blokuje każdą inną transakcję na tych danych // Używać oszczędnie - wpływ na wydajność Account account = accountRepository.findById(accountId).orElseThrow(); account.performCriticalUpdate(); accountRepository.save(account); } } ``` Tabela podsumowująca poziomy izolacji: ```text | Izolacja | Dirty Read | Non-Repeatable | Phantom | |------------------|------------|----------------|---------| | READ_UNCOMMITTED | Możliwe | Możliwe | Możliwe | | READ_COMMITTED | Nie | Możliwe | Możliwe | | REPEATABLE_READ | Nie | Nie | Możliwe | | SERIALIZABLE | Nie | Nie | Nie | ``` > **Wpływ na wydajność** > > Im bardziej rygorystyczna izolacja, tym bardziej wydajność może być dotknięta przez locki. SERIALIZABLE może powodować znaczącą rywalizację w produkcji o wysokim ruchu. ## Zaawansowane wzorce ### Pytanie 10: Jak zaimplementować wzorzec „transactional outbox"? Wzorzec outbox gwarantuje spójność między modyfikacjami w bazie danych a wysyłaniem wiadomości/zdarzeń, nawet w przypadku awarii. ```java // OutboxService.java // Wzorzec Transactional Outbox @Service public class OutboxService { private final OutboxRepository outboxRepository; // Zapisuje zdarzenie w tej samej transakcji co encja biznesowa @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) { // Utworzenie zamówienia Order order = new Order(request); orderRepository.save(order); // Zdarzenie outbox w TEJ SAMEJ transakcji (MANDATORY) // Jeśli commit się powiedzie → oba są utrwalone // Jeśli rollback → żadne nie jest utrwalone outboxService.saveEvent( "ORDER", order.getId(), "ORDER_CREATED", toJson(new OrderCreatedEvent(order)) ); return order; } } // OutboxPublisher.java // Oddzielny proces publikujący zdarzenia @Service public class OutboxPublisher { private final OutboxRepository outboxRepository; private final MessageBroker messageBroker; // Niezależna transakcja dla każdej publikacji @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); } } } ``` ### Pytanie 11: Jak testować różne zachowania propagacji? Testy propagacji wymagają szczególnej uwagi, aby zweryfikować oczekiwane zachowanie transakcyjne. ```java // TransactionPropagationTest.java // Testowanie zachowań propagacji @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 - płatność w tej samej transakcji (REQUIRED) paymentService.processPayment(order.getId(), BigDecimal.TEN); // Then - oba widoczne przed commitem 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 w REQUIRES_NEW orderId = orderService.createOrderWithAudit(new OrderRequest()); throw new RuntimeException("Simulated failure after audit"); } catch (RuntimeException e) { // Główna transakcja robi rollback } // Then - audit (REQUIRES_NEW) nadal zatwierdzony assertThat(findAuditLog(orderId)).isNotNull(); // Ale zamówienie zostało cofnięte assertThat(findOrder(orderId)).isNull(); } @Test void mandatory_shouldThrowWithoutTransaction() { // Given - brak aktywnej transakcji // When/Then - musi rzucić wyjątek assertThatThrownBy(() -> auditService.logCriticalAction("TEST", 1L)) .isInstanceOf(IllegalTransactionStateException.class) .hasMessageContaining("No existing transaction"); } } ``` ```java // PropagationIntegrationTest.java // Test integracyjny z rzeczywistym rollbackiem i commitem @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 - przetwarzanie batchowe z NESTED BatchResult result = orderService.processBatchWithNested( List.of(validItem(), invalidItem(), validItem()) ); // Then - tylko poprawne elementy są utrwalone assertThat(result.getSuccessCount()).isEqualTo(2); assertThat(result.getFailureCount()).isEqualTo(1); assertThat(orderRepository.findAll().size()).isEqualTo(initialCount + 2); } } ``` ### Pytanie 12: Jakie są najlepsze praktyki konfiguracji transakcji? Spójna konfiguracja transakcyjna na poziomie projektu unika niespodzianek i ułatwia utrzymanie. ```java // TransactionConfig.java // Scentralizowana konfiguracja transakcji @Configuration @EnableTransactionManagement public class TransactionConfig { // Domyślny timeout dla wszystkich transakcji @Bean public PlatformTransactionManager transactionManager(DataSource dataSource) { DataSourceTransactionManager tm = new DataSourceTransactionManager(dataSource); tm.setDefaultTimeout(30); // 30 sekund maksymalnie na transakcję return tm; } } // BaseService.java // Domyślne adnotacje dla serwisów @Service @Transactional( readOnly = true, // Tylko do odczytu domyślnie rollbackFor = Exception.class // Rollback przy każdym wyjątku ) public abstract class BaseService { // Metody odczytu dziedziczą readOnly = true } // OrderService.java // Serwis ze spójną konfiguracją @Service public class OrderService extends BaseService { private final OrderRepository orderRepository; // Dziedziczy readOnly = true public Order findById(Long id) { return orderRepository.findById(id).orElseThrow(); } // Override dla zapisów @Transactional(readOnly = false) public Order createOrder(OrderRequest request) { return orderRepository.save(new Order(request)); } // Jawna konfiguracja dla przypadków krytycznych @Transactional( readOnly = false, propagation = Propagation.REQUIRES_NEW, isolation = Isolation.SERIALIZABLE, timeout = 10 ) public void criticalOperation(Long orderId) { // Operacja z maksymalną izolacją i krótkim timeoutem } } ``` Checklista najlepszych praktyk: ```text Konfiguracja transakcji - Checklista ✅ readOnly = true domyślnie, jawny override dla zapisów ✅ rollbackFor = Exception.class aby uwzględnić checked exceptions ✅ Odpowiedni timeout w zależności od typu operacji ✅ Unikać wywołań wewnętrznych (@Transactional ignorowane) ✅ REQUIRES_NEW tylko gdy niezależny commit jest konieczny ✅ MANDATORY aby zagwarantować kontekst transakcyjny ✅ Jawne testy zachowań rollbacka ✅ Monitorować długotrwałe transakcje ✅ Dokumentować niestandardowe wybory propagacji ``` ## Podsumowanie Propagacja transakcji to fundamentalne pojęcie oceniane w rozmowach Spring Boot. Kluczowe punkty do zapamiętania: **Typowe rodzaje propagacji:** - ✅ REQUIRED (domyślny): dołącza lub tworzy transakcję - ✅ REQUIRES_NEW: niezależna transakcja, oddzielny commit - ✅ NESTED: savepoint dla częściowego rollbacka - ✅ MANDATORY: wymaga istniejącej transakcji **Pułapki do uniknięcia:** - ✅ Self-invocation: omija proxy, @Transactional ignorowane - ✅ Checked exceptions: brak rollbacka domyślnie - ✅ REQUIRES_NEW na tych samych danych: ryzyko deadlocka - ✅ Brak timeoutu: transakcje zablokowane bezterminowo **Najlepsze praktyki:** - ✅ readOnly = true domyślnie - ✅ rollbackFor = Exception.class systematycznie - ✅ Oddzielne serwisy aby uniknąć self-invocation - ✅ Jawnie testować zachowania rollbacka Opanowanie propagacji transakcji pokazuje głębokie zrozumienie Springa i zarządzania danymi. Te koncepcje są niezbędne do projektowania solidnych aplikacji i pomyślnego przechodzenia rozmów technicznych. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/pl/blog/spring-boot/spring-boot-transaction-propagation-interview