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.

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.
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ą?"
// 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.
// 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:
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 anulowanePytanie 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.
// 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:
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 zachowanyREQUIRES_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.
// NESTED: savepoint w transakcji nadrzędnej
@Service
public class BatchProcessingService {
private final ItemRepository itemRepository;
private final ItemProcessor itemProcessor;
@Transactional
public BatchResult processBatch(List<Item> 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:
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łączeniaPropagacja 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.
// 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
}
}// 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.
// 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);
}
}// 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);
}
}Gotowy na rozmowy o Spring Boot?
Ćwicz z naszymi interaktywnymi symulatorami, flashcards i testami technicznymi.
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.
// KLASYCZNY BŁĄD: self-invocation
@Service
public class BrokenService {
private final ItemRepository itemRepository;
public void processItems(List<Long> 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:
// Rozwiązanie 1: Self-injection
@Service
public class FixedServiceWithSelfInjection {
private final ItemRepository itemRepository;
@Lazy
@Autowired
private FixedServiceWithSelfInjection self; // Self-injection
public void processItems(List<Long> 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<Long> 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.
// 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:
// 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.
// 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:
| 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 |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.
// 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<OutboxEvent> 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.
// 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");
}
}// 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.
// 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:
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 propagacjiGotowy na rozmowy o Spring Boot?
Ćwicz z naszymi interaktywnymi symulatorami, flashcards i testami technicznymi.
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.
Zacznij ćwiczyć!
Sprawdź swoją wiedzę z naszymi symulatorami rozmów i testami technicznymi.
Tagi
Udostępnij
Powiązane artykuły

30 Pytań Rekrutacyjnych ze Spring Boot: Kompletny Przewodnik dla Programistów Java
Przygotuj się do rozmów rekrutacyjnych ze Spring Boot dzięki 30 kluczowym pytaniom o auto-konfigurację, startery, Spring Data JPA, bezpieczeństwo i testy.

Spring Modulith: Architektura Modularnego Monolitu Wyjaśniona
Naucz się Spring Modulith do budowy modularnych monolitów w Javie. Architektura, moduły, eventy asynchroniczne i testy z przykładami Spring Boot 3.

Rozmowa Spring Batch 5: Partycjonowanie, Chunki i Tolerancja Błędów
Opanuj rozmowy o pracę ze Spring Batch 5: 15 kluczowych pytań o partycjonowanie, przetwarzanie chunkowe i tolerancję błędów z przykładami w Java 21.