Spring Boot Interview: Transaktions-Propagation erklärt

Beherrsche die Spring Boot Transaktions-Propagation: REQUIRED, REQUIRES_NEW, NESTED und mehr. 12 Interview-Fragen mit Code und typischen Fallstricken.

Spring Boot Transaktions-Propagation: Interview-Fragen und praktische Beispiele

Die Transaktions-Propagation stellt ein grundlegendes Konzept in Spring Boot dar und wird regelmäßig in technischen Interviews abgefragt. Das Verständnis, wie Transaktionen zwischen @Transactional-annotierten Methoden interagieren, hilft, subtile Bugs in der Produktion zu vermeiden und robuste Architekturen zu entwerfen.

Vorbereitungshinweis

Interviewer prüfen die Fähigkeit, das richtige Propagationslevel basierend auf dem Geschäftskontext zu wählen. Erklären zu können, warum REQUIRES_NEW statt REQUIRED in einem konkreten Fall, macht den Unterschied.

Grundlagen der Transaktions-Propagation

Frage 1: Was ist Transaktions-Propagation in Spring?

Propagation definiert das Verhalten einer transaktionalen Methode, wenn sie im Kontext einer bestehenden Transaktion aufgerufen wird. Sie beantwortet die Frage: „Was passiert, wenn eine @Transactional-Methode eine andere ebenfalls annotierte Methode aufruft?"

OrderService.javajava
// Demonstration des Propagationskonzepts
@Service
public class OrderService {

    private final PaymentService paymentService;
    private final OrderRepository orderRepository;

    // Eltern-Transaktion - startet eine neue Transaktion
    @Transactional
    public void createOrder(OrderRequest request) {
        // Speichert die Bestellung in der aktuellen Transaktion
        Order order = orderRepository.save(new Order(request));

        // Aufruf einer anderen @Transactional-Methode
        // Propagation entscheidet: gleiche Transaktion oder neue?
        paymentService.processPayment(order.getId(), request.getAmount());
    }
}

@Service
public class PaymentService {

    // Standard-Propagation: REQUIRED
    // Tritt der bestehenden Transaktion von createOrder() bei
    @Transactional
    public void processPayment(Long orderId, BigDecimal amount) {
        // Wird in der GLEICHEN Transaktion wie createOrder() ausgeführt
        // Schlägt diese Methode fehl, wird auch die Bestellung zurückgerollt
    }
}

Spring bietet sieben Propagationslevel, jedes für spezifische Geschäftsanforderungen geeignet. Die Wahl wirkt sich direkt auf Datenkonsistenz und Performance aus.

Frage 2: Beschreibe das Verhalten von REQUIRED (Standard-Propagation)

REQUIRED ist die Standard-Propagation. Existiert eine Transaktion, tritt die Methode dieser bei. Andernfalls wird eine neue erstellt. Das ist das häufigste und intuitivste Verhalten.

UserService.javajava
// REQUIRED: Standardverhalten
@Service
public class UserService {

    private final UserRepository userRepository;
    private final AuditService auditService;

    @Transactional(propagation = Propagation.REQUIRED)
    public void updateUser(Long userId, UserUpdateRequest request) {
        // Startet eine Transaktion, falls keine existiert
        User user = userRepository.findById(userId).orElseThrow();
        user.setEmail(request.getEmail());
        userRepository.save(user);

        // Audit tritt der gleichen Transaktion bei
        auditService.logUpdate(userId, "EMAIL_CHANGED");
    }
}

@Service
public class AuditService {

    private final AuditLogRepository auditLogRepository;

    @Transactional(propagation = Propagation.REQUIRED)
    public void logUpdate(Long userId, String action) {
        // Tritt der Transaktion von updateUser() bei
        // Commit oder Rollback gemeinsam
        auditLogRepository.save(new AuditLog(userId, action, Instant.now()));
    }
}

Das folgende Diagramm veranschaulicht den transaktionalen Ablauf:

text
updateUser() startet TX-1
    ├── save(user)          → TX-1
    └── logUpdate()         → tritt TX-1 bei (REQUIRED)
           └── save(audit)  → TX-1

Wenn logUpdate() fehlschlägt → Rollback TX-1 → user UND audit verworfen

Frage 3: Wann sollte man REQUIRES_NEW statt REQUIRED verwenden?

REQUIRES_NEW suspendiert die bestehende Transaktion und erstellt eine neue, unabhängige Transaktion. Nützlich, wenn eine Operation unabhängig vom Ergebnis der Eltern-Transaktion committet werden muss.

PaymentService.javajava
// REQUIRES_NEW: unabhängige Transaktion
@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);

        // Audit MUSS persistiert werden, auch wenn die Zahlung später fehlschlägt
        auditService.logPaymentAttempt(orderId, amount);

        // Simuliert einen Fehler nach dem Audit
        if (amount.compareTo(BigDecimal.ZERO) < 0) {
            throw new InvalidAmountException("Negative amount");
        }
    }
}

@Service
public class PaymentAuditService {

    private final PaymentAuditRepository auditRepository;

    // REQUIRES_NEW: committet unabhängig von der Eltern-Transaktion
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void logPaymentAttempt(Long orderId, BigDecimal amount) {
        // Neue Transaktion TX-2 erstellt
        // TX-1 (processPayment) ist suspendiert
        auditRepository.save(new PaymentAudit(orderId, amount, "ATTEMPTED"));
        // TX-2 committet hier, unabhängig von TX-1
    }
}

Der transaktionale Ablauf mit REQUIRES_NEW:

text
processPayment() startet TX-1
    ├── save(payment)              → TX-1
    ├── logPaymentAttempt()        → TX-1 SUSPENDIERT
    │       └── startet TX-2       → neue Transaktion
    │       └── save(audit)        → TX-2
    │       └── COMMIT TX-2        → audit persistiert
    │       └── TX-1 SETZT FORT
    └── throw InvalidAmountException
        └── ROLLBACK TX-1          → Zahlung verworfen, aber audit erhalten
Vorsicht vor Deadlocks

REQUIRES_NEW kann Deadlocks verursachen, wenn die neue Transaktion auf dieselben Ressourcen zugreift, die von der suspendierten Transaktion gesperrt sind. Vermeide REQUIRES_NEW, um dieselben Tabellen wie die Eltern-Transaktion zu modifizieren.

Frage 4: Erkläre die NESTED-Propagation und den Unterschied zu REQUIRES_NEW

NESTED erstellt einen Savepoint innerhalb der aktuellen Transaktion. Schlägt die Methode fehl, werden nur die Änderungen seit dem Savepoint zurückgerollt, nicht die gesamte Eltern-Transaktion.

BatchProcessingService.javajava
// NESTED: Savepoint innerhalb der Eltern-Transaktion
@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 {
                // Jedes Item wird mit einem Savepoint verarbeitet
                itemProcessor.processItem(item);
                result.addSuccess(item.getId());
            } catch (ProcessingException e) {
                // Rollback nur dieses Items, nicht des gesamten Batches
                result.addFailure(item.getId(), e.getMessage());
            }
        }

        return result; // Commit erfolgreicher Items
    }
}

@Service
public class ItemProcessor {

    private final ItemRepository itemRepository;

    // NESTED: erstellt einen Savepoint, partielles Rollback möglich
    @Transactional(propagation = Propagation.NESTED)
    public void processItem(Item item) {
        item.setStatus("PROCESSING");
        itemRepository.save(item);

        // Geschäftsvalidierung
        if (!isValid(item)) {
            throw new ProcessingException("Invalid item");
            // Rollback zum Savepoint → nur dieses Item
        }

        item.setStatus("COMPLETED");
        itemRepository.save(item);
    }
}

Vergleich von NESTED vs. REQUIRES_NEW:

text
NESTED:
├── Verwendet einen Savepoint innerhalb der Eltern-TX
├── Bei Fehler → Rollback zum Savepoint
├── Wenn Eltern-TX zurückgerollt wird → NESTED ebenfalls
└── Performanter (keine neue Verbindung)

REQUIRES_NEW:
├── Erstellt eine völlig unabhängige Transaktion
├── Bei Fehler → Rollback nur der Kind-TX
├── Wenn Eltern-TX zurückgerollt wird → Kind-TX BEREITS COMMITTET
└── Erfordert eine neue Verbindung
NESTED-Unterstützung

NESTED-Propagation erfordert JDBC-Savepoint-Unterstützung. Die meisten modernen Datenbanken (PostgreSQL, MySQL, Oracle) unterstützen dies. Kompatibilität vor der Verwendung prüfen.

Erweiterte Propagationstypen

Frage 5: Wann sollte man SUPPORTS und NOT_SUPPORTED verwenden?

SUPPORTS führt innerhalb der bestehenden Transaktion aus, falls vorhanden, sonst ohne Transaktion. NOT_SUPPORTED suspendiert jede bestehende Transaktion und führt ohne Transaktion aus.

ReportingService.javajava
// SUPPORTS: optionale Transaktion
@Service
public class ReportingService {

    private final ReportRepository reportRepository;

    // SUPPORTS: funktioniert mit oder ohne Transaktion
    // Nützlich für Lesezugriffe, die keine transaktionalen Garantien brauchen
    @Transactional(propagation = Propagation.SUPPORTS)
    public Report generateReport(Long reportId) {
        // Bei Aufruf aus einer @Transactional-Methode → nutzt deren TX
        // Bei direktem Aufruf → keine Transaktion (read-only OK)
        return reportRepository.generateComplexReport(reportId);
    }
}

@Service
public class ExternalNotificationService {

    private final ExternalApiClient apiClient;

    // NOT_SUPPORTED: niemals in einer Transaktion
    // Verhindert TX-Blockade während eines langsamen externen Aufrufs
    @Transactional(propagation = Propagation.NOT_SUPPORTED)
    public void sendExternalNotification(String message) {
        // Eltern-TX während des Aufrufs suspendiert
        apiClient.send(message); // Potenziell langsamer HTTP-Aufruf
        // Eltern-TX wird danach fortgesetzt
    }
}
OrderService.javajava
// Beispiel für kombinierte Verwendung
@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);

        // Report-Generierung in der gleichen TX (SUPPORTS)
        Report report = reportingService.generateReport(orderId);

        // Externe Benachrichtigung AUSSERHALB der Transaktion (NOT_SUPPORTED)
        // Verhindert TX-Timeout, falls externe API langsam ist
        notificationService.sendExternalNotification(
            "Order " + orderId + " completed"
        );
    }
}

Frage 6: Erkläre MANDATORY und NEVER

MANDATORY erfordert eine bestehende Transaktion und wirft sonst eine Exception. NEVER erfordert das Fehlen einer Transaktion und wirft eine Exception, falls eine existiert.

AuditService.javajava
// MANDATORY: muss aus einer Transaktion heraus aufgerufen werden
@Service
public class AuditService {

    private final AuditRepository auditRepository;

    // MANDATORY: weigert sich, ohne bestehende Transaktion zu laufen
    // Garantiert, dass das Audit immer atomar mit der auditierten Operation ist
    @Transactional(propagation = Propagation.MANDATORY)
    public void logCriticalAction(String action, Long entityId) {
        // Aufruf ohne Transaktion → IllegalTransactionStateException
        auditRepository.save(new AuditLog(action, entityId, Instant.now()));
    }
}

// CacheService.java
// NEVER: darf niemals in einer Transaktion sein
@Service
public class CacheService {

    private final CacheManager cacheManager;

    // NEVER: Cache sollte nicht an Transaktionen teilnehmen
    // Verhindert Inkonsistenzen zwischen Cache und DB nach Rollback
    @Transactional(propagation = Propagation.NEVER)
    public void invalidateCache(String cacheKey) {
        // Aufruf aus @Transactional → IllegalTransactionStateException
        cacheManager.getCache("entities").evict(cacheKey);
    }
}
SecurityService.javajava
// Korrekte Verwendung von MANDATORY
@Service
public class SecurityService {

    private final AuditService auditService;

    @Transactional
    public void changeUserPassword(Long userId, String newPassword) {
        // Sensible Operation...
        updatePassword(userId, newPassword);

        // Audit MUSS in derselben Transaktion sein
        // MANDATORY erzwingt diese Architekturvorgabe
        auditService.logCriticalAction("PASSWORD_CHANGE", userId);
    }

    // FEHLER: direkter Aufruf ohne Transaktion
    public void badUsage() {
        // Wirft IllegalTransactionStateException, da keine TX vorhanden
        auditService.logCriticalAction("TEST", 1L);
    }
}

Bereit für deine Spring Boot-Interviews?

Übe mit unseren interaktiven Simulatoren, Flashcards und technischen Tests.

Häufige Interview-Fallstricke

Frage 7: Warum funktioniert @Transactional bei internen Aufrufen nicht?

Einer der häufigsten Fallstricke. Interne Methodenaufrufe (Self-Invocation) umgehen den Spring-Proxy und deaktivieren das Transaktionsmanagement.

BrokenService.javajava
// KLASSISCHER FEHLER: Self-Invocation
@Service
public class BrokenService {

    private final ItemRepository itemRepository;

    public void processItems(List<Long> itemIds) {
        for (Long id : itemIds) {
            // FALLE: interner Aufruf → umgeht den Proxy
            // @Transactional auf processItem() wird IGNORIERT
            processItem(id);
        }
    }

    @Transactional
    public void processItem(Long itemId) {
        // Diese Transaktion wird bei internen Aufrufen NIE erstellt
        Item item = itemRepository.findById(itemId).orElseThrow();
        item.setStatus("PROCESSED");
        itemRepository.save(item);
    }
}

Lösungen, um diese Falle zu vermeiden:

java
// Lösung 1: Selbstinjektion
@Service
public class FixedServiceWithSelfInjection {

    private final ItemRepository itemRepository;

    @Lazy
    @Autowired
    private FixedServiceWithSelfInjection self; // Selbstinjektion

    public void processItems(List<Long> itemIds) {
        for (Long id : itemIds) {
            // Aufruf über Proxy → @Transactional funktioniert
            self.processItem(id);
        }
    }

    @Transactional
    public void processItem(Long itemId) {
        // Transaktion korrekt erstellt
        Item item = itemRepository.findById(itemId).orElseThrow();
        item.setStatus("PROCESSED");
        itemRepository.save(item);
    }
}

// Lösung 2: Aufteilung in zwei Services (empfohlen)
@Service
public class ItemOrchestrator {

    private final ItemProcessor processor;

    public void processItems(List<Long> itemIds) {
        for (Long id : itemIds) {
            // Aufruf an anderes Bean → Proxy funktioniert
            processor.processItem(id);
        }
    }
}

@Service
public class ItemProcessor {

    private final ItemRepository itemRepository;

    @Transactional
    public void processItem(Long itemId) {
        // Transaktion korrekt verwaltet
        Item item = itemRepository.findById(itemId).orElseThrow();
        item.setStatus("PROCESSED");
        itemRepository.save(item);
    }
}

Frage 8: Wie behandelt man Exceptions und Rollback?

Standardmäßig führt Spring nur bei RuntimeException und Error ein Rollback durch. Checked Exceptions lösen KEIN automatisches Rollback aus.

TransactionRollbackDemo.javajava
// Rollback-Verhalten je nach Exception-Typ
@Service
public class TransactionRollbackDemo {

    private final OrderRepository orderRepository;

    // RuntimeException → automatisches ROLLBACK
    @Transactional
    public void methodWithRuntimeException() {
        orderRepository.save(new Order());
        throw new RuntimeException("Error"); // ROLLBACK
    }

    // Checked Exception → standardmäßig KEIN Rollback
    @Transactional
    public void methodWithCheckedException() throws IOException {
        orderRepository.save(new Order());
        throw new IOException("File error"); // COMMITTET trotzdem!
    }

    // Rollback bei Checked Exception erzwingen
    @Transactional(rollbackFor = IOException.class)
    public void methodWithRollbackFor() throws IOException {
        orderRepository.save(new Order());
        throw new IOException("Error"); // ROLLBACK dank rollbackFor
    }

    // Eine RuntimeException vom Rollback ausschließen
    @Transactional(noRollbackFor = BusinessException.class)
    public void methodWithNoRollbackFor() {
        orderRepository.save(new Order());
        throw new BusinessException("Warning"); // COMMITTET trotz Exception
    }
}

Empfohlene Konfiguration für Geschäftsfälle:

BaseTransactionalService.javajava
// Konsistente transaktionale Konfiguration
@Service
public abstract class BaseTransactionalService {

    // Rollback bei allen Exceptions (checked und 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) {
            // KEIN Rollback - der Versuch soll als Datensatz erhalten bleiben
            throw new InsufficientFundsException("Insufficient balance");
        }

        return new PaymentResult(payment.getId(), "SUCCESS");
    }
}

Frage 9: Wie funktioniert Transaktionsisolation mit Propagation?

Isolation und Propagation ergänzen sich. Isolation bestimmt die Datensichtbarkeit zwischen nebenläufigen Transaktionen.

IsolationDemo.javajava
// Kombination Isolation + Propagation
@Service
public class IsolationDemo {

    private final AccountRepository accountRepository;

    // READ_COMMITTED: sieht von anderen Transaktionen committete Daten
    @Transactional(
        isolation = Isolation.READ_COMMITTED,
        propagation = Propagation.REQUIRED
    )
    public BigDecimal getAccountBalance(Long accountId) {
        // Kann unterschiedliche Werte sehen, wenn während der TX erneut gelesen
        return accountRepository.findById(accountId)
            .map(Account::getBalance)
            .orElse(BigDecimal.ZERO);
    }

    // REPEATABLE_READ: garantiert dieselbe Lesung während der Transaktion
    @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();

        // Selbst wenn eine andere Transaktion 'from' zwischenzeitlich ändert,
        // sehen wir immer den initialen Wert (Snapshot)
        from.debit(amount);
        to.credit(amount);

        accountRepository.save(from);
        accountRepository.save(to);
    }

    // SERIALIZABLE: maximale Isolation, keine Nebenläufigkeit
    @Transactional(
        isolation = Isolation.SERIALIZABLE,
        propagation = Propagation.REQUIRES_NEW
    )
    public void criticalOperation(Long accountId) {
        // Blockiert jede andere Transaktion auf diesen Daten
        // Sparsam einsetzen - Performance-Auswirkung
        Account account = accountRepository.findById(accountId).orElseThrow();
        account.performCriticalUpdate();
        accountRepository.save(account);
    }
}

Übersichtstabelle der Isolationslevel:

text
| Isolation        | Dirty Read | Non-Repeatable | Phantom |
|------------------|------------|----------------|---------|
| READ_UNCOMMITTED | Möglich    | Möglich        | Möglich |
| READ_COMMITTED   | Nein       | Möglich        | Möglich |
| REPEATABLE_READ  | Nein       | Nein           | Möglich |
| SERIALIZABLE     | Nein       | Nein           | Nein    |
Performance-Auswirkung

Je strenger die Isolation, desto stärker wird die Performance durch Locks beeinflusst. SERIALIZABLE kann in stark frequentierten Produktionsumgebungen zu erheblichen Konflikten führen.

Erweiterte Patterns

Frage 10: Wie implementiert man das „Transactional Outbox"-Pattern?

Das Outbox-Pattern garantiert Konsistenz zwischen Datenbankänderungen und dem Versenden von Nachrichten/Events, selbst im Fehlerfall.

OutboxService.javajava
// Transactional Outbox Pattern
@Service
public class OutboxService {

    private final OutboxRepository outboxRepository;

    // Speichert das Event in derselben Transaktion wie die Geschäftsentität
    @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) {
        // Bestellung erstellen
        Order order = new Order(request);
        orderRepository.save(order);

        // Outbox-Event in DERSELBEN Transaktion (MANDATORY)
        // Bei erfolgreichem Commit → beide werden persistiert
        // Bei Rollback → keines wird persistiert
        outboxService.saveEvent(
            "ORDER",
            order.getId(),
            "ORDER_CREATED",
            toJson(new OrderCreatedEvent(order))
        );

        return order;
    }
}

// OutboxPublisher.java
// Separater Prozess, der Events veröffentlicht
@Service
public class OutboxPublisher {

    private final OutboxRepository outboxRepository;
    private final MessageBroker messageBroker;

    // Unabhängige Transaktion für jede Veröffentlichung
    @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);
        }
    }
}

Frage 11: Wie testet man verschiedene Propagationsverhalten?

Propagationstests erfordern besondere Aufmerksamkeit, um das erwartete transaktionale Verhalten zu verifizieren.

TransactionPropagationTest.javajava
// Testen von Propagationsverhalten
@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 - Zahlung in derselben Transaktion (REQUIRED)
        paymentService.processPayment(order.getId(), BigDecimal.TEN);

        // Then - beide vor dem Commit sichtbar
        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 in REQUIRES_NEW
            orderId = orderService.createOrderWithAudit(new OrderRequest());
            throw new RuntimeException("Simulated failure after audit");
        } catch (RuntimeException e) {
            // Haupttransaktion wird zurückgerollt
        }

        // Then - Audit (REQUIRES_NEW) ist trotzdem committet
        assertThat(findAuditLog(orderId)).isNotNull();
        // Aber die Bestellung wird zurückgerollt
        assertThat(findOrder(orderId)).isNull();
    }

    @Test
    void mandatory_shouldThrowWithoutTransaction() {
        // Given - keine aktive Transaktion

        // When/Then - muss eine Exception werfen
        assertThatThrownBy(() -> auditService.logCriticalAction("TEST", 1L))
            .isInstanceOf(IllegalTransactionStateException.class)
            .hasMessageContaining("No existing transaction");
    }
}
PropagationIntegrationTest.javajava
// Integrationstest mit echtem Rollback und 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-Verarbeitung mit NESTED
        BatchResult result = orderService.processBatchWithNested(
            List.of(validItem(), invalidItem(), validItem())
        );

        // Then - nur gültige Items werden persistiert
        assertThat(result.getSuccessCount()).isEqualTo(2);
        assertThat(result.getFailureCount()).isEqualTo(1);
        assertThat(orderRepository.findAll().size()).isEqualTo(initialCount + 2);
    }
}

Frage 12: Was sind die Best Practices für die Transaktionskonfiguration?

Eine konsistente transaktionale Konfiguration auf Projektebene vermeidet Überraschungen und erleichtert die Wartung.

TransactionConfig.javajava
// Zentralisierte Transaktionskonfiguration
@Configuration
@EnableTransactionManagement
public class TransactionConfig {

    // Standard-Timeout für alle Transaktionen
    @Bean
    public PlatformTransactionManager transactionManager(DataSource dataSource) {
        DataSourceTransactionManager tm = new DataSourceTransactionManager(dataSource);
        tm.setDefaultTimeout(30); // 30 Sekunden max pro Transaktion
        return tm;
    }
}

// BaseService.java
// Standardannotationen für Services
@Service
@Transactional(
    readOnly = true, // Standardmäßig nur lesend
    rollbackFor = Exception.class // Rollback bei jeder Exception
)
public abstract class BaseService {
    // Lesemethoden erben readOnly = true
}

// OrderService.java
// Service mit konsistenter Konfiguration
@Service
public class OrderService extends BaseService {

    private final OrderRepository orderRepository;

    // Erbt readOnly = true
    public Order findById(Long id) {
        return orderRepository.findById(id).orElseThrow();
    }

    // Override für Schreibzugriffe
    @Transactional(readOnly = false)
    public Order createOrder(OrderRequest request) {
        return orderRepository.save(new Order(request));
    }

    // Explizite Konfiguration für kritische Fälle
    @Transactional(
        readOnly = false,
        propagation = Propagation.REQUIRES_NEW,
        isolation = Isolation.SERIALIZABLE,
        timeout = 10
    )
    public void criticalOperation(Long orderId) {
        // Operation mit maximaler Isolation und kurzem Timeout
    }
}

Best-Practices-Checkliste:

text
Transaktionskonfiguration - Checkliste

✅ readOnly = true standardmäßig, expliziter Override für Schreibzugriffe
✅ rollbackFor = Exception.class um Checked Exceptions einzuschließen
✅ Passender Timeout je nach Operationstyp
✅ Interne Aufrufe vermeiden (@Transactional ignoriert)
✅ REQUIRES_NEW nur, wenn unabhängiger Commit notwendig ist
✅ MANDATORY um transaktionalen Kontext zu garantieren
✅ Explizite Tests für Rollback-Verhalten
✅ Lange laufende Transaktionen überwachen
✅ Nicht-standardmäßige Propagationsentscheidungen dokumentieren

Bereit für deine Spring Boot-Interviews?

Übe mit unseren interaktiven Simulatoren, Flashcards und technischen Tests.

Fazit

Transaktions-Propagation ist ein grundlegendes Konzept, das in Spring Boot Interviews abgefragt wird. Schlüsselpunkte zum Merken:

Häufige Propagationstypen:

  • ✅ REQUIRED (Standard): tritt einer Transaktion bei oder erstellt eine
  • ✅ REQUIRES_NEW: unabhängige Transaktion, separater Commit
  • ✅ NESTED: Savepoint für partielles Rollback
  • ✅ MANDATORY: erfordert eine bestehende Transaktion

Zu vermeidende Fallstricke:

  • ✅ Self-Invocation: umgeht Proxy, @Transactional ignoriert
  • ✅ Checked Exceptions: standardmäßig kein Rollback
  • ✅ REQUIRES_NEW auf denselben Daten: Deadlock-Risiko
  • ✅ Fehlender Timeout: Transaktionen unbegrenzt blockiert

Best Practices:

  • ✅ readOnly = true standardmäßig
  • ✅ rollbackFor = Exception.class systematisch
  • ✅ Services trennen, um Self-Invocation zu vermeiden
  • ✅ Rollback-Verhalten explizit testen

Die Beherrschung der Transaktions-Propagation zeigt ein tiefes Verständnis von Spring und Datenmanagement. Diese Konzepte sind entscheidend, um robuste Anwendungen zu entwerfen und technische Interviews erfolgreich zu bestehen.

Fang an zu üben!

Teste dein Wissen mit unseren Interview-Simulatoren und technischen Tests.

Tags

#spring boot
#transactions
#propagation
#java
#interview

Teilen

Verwandte Artikel