Spring Boot Sollicitatiegesprek: Transactiepropagatie

Beheers Spring Boot transactiepropagatie: REQUIRED, REQUIRES_NEW, NESTED en meer. 12 sollicitatievragen met code en veelgemaakte valkuilen.

Spring Boot Transactiepropagatie: sollicitatievragen en praktische voorbeelden

Transactiepropagatie vormt een fundamenteel concept in Spring Boot, dat regelmatig wordt geëvalueerd tijdens technische sollicitatiegesprekken. Begrijpen hoe transacties op elkaar inwerken tussen @Transactional-geannoteerde methoden helpt om subtiele productiebugs te vermijden en robuuste architecturen te ontwerpen.

Voorbereidingsadvies

Interviewers testen het vermogen om het juiste propagatieniveau te kiezen op basis van de bedrijfscontext. Kunnen uitleggen waarom REQUIRES_NEW in plaats van REQUIRED in een specifiek geval maakt het verschil.

Fundamenten van transactiepropagatie

Vraag 1: Wat is transactiepropagatie in Spring?

Propagatie definieert het gedrag van een transactionele methode wanneer deze wordt aangeroepen binnen de context van een bestaande transactie. Het beantwoordt de vraag: "Wat gebeurt er wanneer een @Transactional-methode een andere geannoteerde methode aanroept?"

OrderService.javajava
// Demonstratie van het propagatieconcept
@Service
public class OrderService {

    private final PaymentService paymentService;
    private final OrderRepository orderRepository;

    // Oudertransactie - start een nieuwe transactie
    @Transactional
    public void createOrder(OrderRequest request) {
        // Slaat de bestelling op in de huidige transactie
        Order order = orderRepository.save(new Order(request));

        // Aanroep van een andere @Transactional-methode
        // Propagatie bepaalt: dezelfde transactie of nieuwe?
        paymentService.processPayment(order.getId(), request.getAmount());
    }
}

@Service
public class PaymentService {

    // Standaard propagatie: REQUIRED
    // Sluit zich aan bij de bestaande transactie van createOrder()
    @Transactional
    public void processPayment(Long orderId, BigDecimal amount) {
        // Voert uit in DEZELFDE transactie als createOrder()
        // Als deze methode faalt, wordt ook de bestelling teruggedraaid
    }
}

Spring biedt zeven propagatieniveaus, elk geschikt voor specifieke bedrijfsbehoeften. De keuze heeft directe impact op data-consistentie en performance.

Vraag 2: Beschrijf het gedrag van REQUIRED (standaard propagatie)

REQUIRED is de standaard propagatie. Als er een transactie bestaat, sluit de methode zich daarbij aan. Anders wordt een nieuwe gemaakt. Dit is het meest voorkomende en intuïtieve gedrag.

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

    private final UserRepository userRepository;
    private final AuditService auditService;

    @Transactional(propagation = Propagation.REQUIRED)
    public void updateUser(Long userId, UserUpdateRequest request) {
        // Start een transactie als er geen bestaat
        User user = userRepository.findById(userId).orElseThrow();
        user.setEmail(request.getEmail());
        userRepository.save(user);

        // De audit sluit zich aan bij dezelfde transactie
        auditService.logUpdate(userId, "EMAIL_CHANGED");
    }
}

@Service
public class AuditService {

    private final AuditLogRepository auditLogRepository;

    @Transactional(propagation = Propagation.REQUIRED)
    public void logUpdate(Long userId, String action) {
        // Sluit zich aan bij de transactie van updateUser()
        // Commit of rollback samen
        auditLogRepository.save(new AuditLog(userId, action, Instant.now()));
    }
}

Het onderstaande diagram illustreert de transactionele flow:

text
updateUser() start TX-1
    ├── save(user)          → TX-1
    └── logUpdate()         → sluit aan bij TX-1 (REQUIRED)
           └── save(audit)  → TX-1

Als logUpdate() faalt → rollback TX-1 → user EN audit geannuleerd

Vraag 3: Wanneer REQUIRES_NEW gebruiken in plaats van REQUIRED?

REQUIRES_NEW pauzeert de bestaande transactie en maakt een nieuwe onafhankelijke transactie. Nuttig wanneer een operatie gecommit moet worden ongeacht het resultaat van de oudertransactie.

PaymentService.javajava
// REQUIRES_NEW: onafhankelijke transactie
@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 MOET worden bewaard zelfs als de betaling later faalt
        auditService.logPaymentAttempt(orderId, amount);

        // Simulatie van een fout na de audit
        if (amount.compareTo(BigDecimal.ZERO) < 0) {
            throw new InvalidAmountException("Negative amount");
        }
    }
}

@Service
public class PaymentAuditService {

    private final PaymentAuditRepository auditRepository;

    // REQUIRES_NEW: commit onafhankelijk van de oudertransactie
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void logPaymentAttempt(Long orderId, BigDecimal amount) {
        // Nieuwe transactie TX-2 gemaakt
        // TX-1 (processPayment) is gepauzeerd
        auditRepository.save(new PaymentAudit(orderId, amount, "ATTEMPTED"));
        // TX-2 commit hier, onafhankelijk van TX-1
    }
}

De transactionele flow met REQUIRES_NEW:

text
processPayment() start TX-1
    ├── save(payment)              → TX-1
    ├── logPaymentAttempt()        → TX-1 GEPAUZEERD
    │       └── start TX-2         → nieuwe transactie
    │       └── save(audit)        → TX-2
    │       └── COMMIT TX-2        → audit bewaard
    │       └── TX-1 HERVAT
    └── throw InvalidAmountException
        └── ROLLBACK TX-1          → betaling geannuleerd, maar audit behouden
Pas op voor deadlocks

REQUIRES_NEW kan deadlocks veroorzaken als de nieuwe transactie toegang krijgt tot dezelfde bronnen die zijn vergrendeld door de gepauzeerde transactie. Vermijd REQUIRES_NEW om dezelfde tabellen als de oudertransactie te wijzigen.

Vraag 4: Leg NESTED-propagatie uit en hoe verschilt deze van REQUIRES_NEW

NESTED maakt een savepoint binnen de huidige transactie. Als de methode faalt, worden alleen de wijzigingen sinds het savepoint teruggedraaid, niet de hele oudertransactie.

BatchProcessingService.javajava
// NESTED: savepoint binnen de oudertransactie
@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 {
                // Elk item wordt verwerkt met een savepoint
                itemProcessor.processItem(item);
                result.addSuccess(item.getId());
            } catch (ProcessingException e) {
                // Rollback alleen van dit item, niet van de hele batch
                result.addFailure(item.getId(), e.getMessage());
            }
        }

        return result; // Commit van succesvolle items
    }
}

@Service
public class ItemProcessor {

    private final ItemRepository itemRepository;

    // NESTED: maakt een savepoint, gedeeltelijke rollback mogelijk
    @Transactional(propagation = Propagation.NESTED)
    public void processItem(Item item) {
        item.setStatus("PROCESSING");
        itemRepository.save(item);

        // Bedrijfsvalidatie
        if (!isValid(item)) {
            throw new ProcessingException("Invalid item");
            // Rollback naar savepoint → alleen dit item
        }

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

Vergelijking NESTED vs REQUIRES_NEW:

text
NESTED:
├── Gebruikt een savepoint binnen ouder-TX
├── Bij fout → rollback naar savepoint
├── Als ouder-TX rollback → NESTED ook rollback
└── Performanter (geen nieuwe verbinding)

REQUIRES_NEW:
├── Maakt een volledig onafhankelijke transactie
├── Bij fout → rollback alleen van kind-TX
├── Als ouder-TX rollback → kind-TX is AL GECOMMIT
└── Vereist een nieuwe verbinding
NESTED-ondersteuning

NESTED-propagatie vereist JDBC-savepoint-ondersteuning. De meeste moderne databases (PostgreSQL, MySQL, Oracle) ondersteunen dit. Verifieer compatibiliteit voor gebruik.

Geavanceerde propagatietypen

Vraag 5: Wanneer SUPPORTS en NOT_SUPPORTED gebruiken?

SUPPORTS voert uit binnen de bestaande transactie indien aanwezig, anders zonder transactie. NOT_SUPPORTED pauzeert elke bestaande transactie en voert uit zonder transactie.

ReportingService.javajava
// SUPPORTS: optionele transactie
@Service
public class ReportingService {

    private final ReportRepository reportRepository;

    // SUPPORTS: werkt met of zonder transactie
    // Nuttig voor leesoperaties die geen transactionele garanties nodig hebben
    @Transactional(propagation = Propagation.SUPPORTS)
    public Report generateReport(Long reportId) {
        // Indien aangeroepen vanuit @Transactional-methode → gebruikt zijn TX
        // Indien direct aangeroepen → geen transactie (lezen OK)
        return reportRepository.generateComplexReport(reportId);
    }
}

@Service
public class ExternalNotificationService {

    private final ExternalApiClient apiClient;

    // NOT_SUPPORTED: nooit binnen een transactie
    // Voorkomt blokkeren van TX tijdens een trage externe aanroep
    @Transactional(propagation = Propagation.NOT_SUPPORTED)
    public void sendExternalNotification(String message) {
        // Ouder-TX gepauzeerd tijdens de aanroep
        apiClient.send(message); // Mogelijk trage HTTP-aanroep
        // Ouder-TX hervat erna
    }
}
OrderService.javajava
// Voorbeeld van gecombineerd gebruik
@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);

        // Rapportgeneratie in dezelfde TX (SUPPORTS)
        Report report = reportingService.generateReport(orderId);

        // Externe notificatie BUITEN transactie (NOT_SUPPORTED)
        // Voorkomt TX-timeout als externe API traag is
        notificationService.sendExternalNotification(
            "Order " + orderId + " completed"
        );
    }
}

Vraag 6: Leg MANDATORY en NEVER uit

MANDATORY vereist een bestaande transactie en gooit anders een exception. NEVER vereist de afwezigheid van een transactie en gooit een exception als er een bestaat.

AuditService.javajava
// MANDATORY: moet vanuit een transactie worden aangeroepen
@Service
public class AuditService {

    private final AuditRepository auditRepository;

    // MANDATORY: weigert uit te voeren zonder bestaande transactie
    // Garandeert dat audit altijd atomair is met de geauditeerde operatie
    @Transactional(propagation = Propagation.MANDATORY)
    public void logCriticalAction(String action, Long entityId) {
        // Indien aangeroepen zonder transactie → IllegalTransactionStateException
        auditRepository.save(new AuditLog(action, entityId, Instant.now()));
    }
}

// CacheService.java
// NEVER: mag nooit binnen een transactie zijn
@Service
public class CacheService {

    private final CacheManager cacheManager;

    // NEVER: cache mag niet deelnemen aan transacties
    // Voorkomt inconsistenties tussen cache en DB na rollback
    @Transactional(propagation = Propagation.NEVER)
    public void invalidateCache(String cacheKey) {
        // Indien aangeroepen vanuit @Transactional → IllegalTransactionStateException
        cacheManager.getCache("entities").evict(cacheKey);
    }
}
SecurityService.javajava
// Correct gebruik van MANDATORY
@Service
public class SecurityService {

    private final AuditService auditService;

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

        // Audit MOET in dezelfde transactie zijn
        // MANDATORY dwingt deze architecturale beperking af
        auditService.logCriticalAction("PASSWORD_CHANGE", userId);
    }

    // FOUT: directe aanroep zonder transactie
    public void badUsage() {
        // Gooit IllegalTransactionStateException omdat er geen TX is
        auditService.logCriticalAction("TEST", 1L);
    }
}

Klaar om je Spring Boot gesprekken te halen?

Oefen met onze interactieve simulatoren, flashcards en technische tests.

Veelvoorkomende sollicitatievalkuilen

Vraag 7: Waarom werkt @Transactional niet bij interne aanroepen?

Een van de meest voorkomende valkuilen. Interne methode-aanroepen (self-invocation) omzeilen de Spring-proxy, waardoor transactiebeheer wordt uitgeschakeld.

BrokenService.javajava
// KLASSIEKE FOUT: self-invocation
@Service
public class BrokenService {

    private final ItemRepository itemRepository;

    public void processItems(List<Long> itemIds) {
        for (Long id : itemIds) {
            // VALKUIL: interne aanroep → omzeilt de proxy
            // @Transactional op processItem() wordt GENEGEERD
            processItem(id);
        }
    }

    @Transactional
    public void processItem(Long itemId) {
        // Deze transactie wordt NOOIT gemaakt bij interne aanroepen
        Item item = itemRepository.findById(itemId).orElseThrow();
        item.setStatus("PROCESSED");
        itemRepository.save(item);
    }
}

Oplossingen om deze valkuil te vermijden:

java
// Oplossing 1: Self-injectie
@Service
public class FixedServiceWithSelfInjection {

    private final ItemRepository itemRepository;

    @Lazy
    @Autowired
    private FixedServiceWithSelfInjection self; // Self-injectie

    public void processItems(List<Long> itemIds) {
        for (Long id : itemIds) {
            // Aanroep via proxy → @Transactional werkt
            self.processItem(id);
        }
    }

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

// Oplossing 2: Splitsen in twee services (aanbevolen)
@Service
public class ItemOrchestrator {

    private final ItemProcessor processor;

    public void processItems(List<Long> itemIds) {
        for (Long id : itemIds) {
            // Aanroep naar andere bean → proxy werkt
            processor.processItem(id);
        }
    }
}

@Service
public class ItemProcessor {

    private final ItemRepository itemRepository;

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

Vraag 8: Hoe omgaan met exceptions en rollback?

Standaard doet Spring alleen rollback bij RuntimeException en Error. Checked exceptions activeren GEEN automatische rollback.

TransactionRollbackDemo.javajava
// Rollback-gedrag op basis van exception-type
@Service
public class TransactionRollbackDemo {

    private final OrderRepository orderRepository;

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

    // Checked Exception → GEEN rollback standaard
    @Transactional
    public void methodWithCheckedException() throws IOException {
        orderRepository.save(new Order());
        throw new IOException("File error"); // COMMIT toch!
    }

    // Forceer rollback bij checked exception
    @Transactional(rollbackFor = IOException.class)
    public void methodWithRollbackFor() throws IOException {
        orderRepository.save(new Order());
        throw new IOException("Error"); // ROLLBACK dankzij rollbackFor
    }

    // Sluit een RuntimeException uit van rollback
    @Transactional(noRollbackFor = BusinessException.class)
    public void methodWithNoRollbackFor() {
        orderRepository.save(new Order());
        throw new BusinessException("Warning"); // COMMIT ondanks exception
    }
}

Aanbevolen configuratie voor bedrijfsgevallen:

BaseTransactionalService.javajava
// Consistente transactionele configuratie
@Service
public abstract class BaseTransactionalService {

    // Rollback bij alle exceptions (checked en 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) {
            // GEEN rollback - we willen het pogingsrecord behouden
            throw new InsufficientFundsException("Insufficient balance");
        }

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

Vraag 9: Hoe werkt transactie-isolatie met propagatie?

Isolatie en propagatie zijn complementair. Isolatie bepaalt de zichtbaarheid van data tussen gelijktijdige transacties.

IsolationDemo.javajava
// Combinatie isolatie + propagatie
@Service
public class IsolationDemo {

    private final AccountRepository accountRepository;

    // READ_COMMITTED: ziet door andere transacties gecommitte data
    @Transactional(
        isolation = Isolation.READ_COMMITTED,
        propagation = Propagation.REQUIRED
    )
    public BigDecimal getAccountBalance(Long accountId) {
        // Kan verschillende waarden zien als opnieuw gelezen tijdens transactie
        return accountRepository.findById(accountId)
            .map(Account::getBalance)
            .orElse(BigDecimal.ZERO);
    }

    // REPEATABLE_READ: garandeert dezelfde lezing tijdens transactie
    @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();

        // Zelfs als een andere transactie 'from' tussentijds wijzigt,
        // zien we altijd de initiële waarde (snapshot)
        from.debit(amount);
        to.credit(amount);

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

    // SERIALIZABLE: maximale isolatie, geen concurrency
    @Transactional(
        isolation = Isolation.SERIALIZABLE,
        propagation = Propagation.REQUIRES_NEW
    )
    public void criticalOperation(Long accountId) {
        // Blokkeert elke andere transactie op deze data
        // Spaarzaam gebruiken - performance-impact
        Account account = accountRepository.findById(accountId).orElseThrow();
        account.performCriticalUpdate();
        accountRepository.save(account);
    }
}

Overzichtstabel van isolatieniveaus:

text
| Isolatie         | Dirty Read | Non-Repeatable | Phantom |
|------------------|------------|----------------|---------|
| READ_UNCOMMITTED | Mogelijk   | Mogelijk       | Mogelijk|
| READ_COMMITTED   | Nee        | Mogelijk       | Mogelijk|
| REPEATABLE_READ  | Nee        | Nee            | Mogelijk|
| SERIALIZABLE     | Nee        | Nee            | Nee     |
Performance-impact

Hoe strikter de isolatie, hoe meer de performance kan worden beïnvloed door locks. SERIALIZABLE kan significante contentie veroorzaken in productie met hoog verkeer.

Geavanceerde patronen

Vraag 10: Hoe het "transactional outbox"-patroon implementeren?

Het outbox-patroon garandeert consistentie tussen databasewijzigingen en het verzenden van berichten/events, zelfs bij falen.

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

    private final OutboxRepository outboxRepository;

    // Slaat het event op in dezelfde transactie als de bedrijfsentiteit
    @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) {
        // De bestelling maken
        Order order = new Order(request);
        orderRepository.save(order);

        // Outbox-event in DEZELFDE transactie (MANDATORY)
        // Als commit slaagt → beide worden bewaard
        // Als rollback → geen wordt bewaard
        outboxService.saveEvent(
            "ORDER",
            order.getId(),
            "ORDER_CREATED",
            toJson(new OrderCreatedEvent(order))
        );

        return order;
    }
}

// OutboxPublisher.java
// Apart proces dat events publiceert
@Service
public class OutboxPublisher {

    private final OutboxRepository outboxRepository;
    private final MessageBroker messageBroker;

    // Onafhankelijke transactie voor elke publicatie
    @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);
        }
    }
}

Vraag 11: Hoe verschillende propagatiegedragingen testen?

Propagatietests vereisen speciale aandacht om het verwachte transactionele gedrag te verifiëren.

TransactionPropagationTest.javajava
// Testen van propagatiegedragingen
@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 - betaling in dezelfde transactie (REQUIRED)
        paymentService.processPayment(order.getId(), BigDecimal.TEN);

        // Then - beide zichtbaar voor 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 in REQUIRES_NEW
            orderId = orderService.createOrderWithAudit(new OrderRequest());
            throw new RuntimeException("Simulated failure after audit");
        } catch (RuntimeException e) {
            // Hoofdtransactie doet rollback
        }

        // Then - audit (REQUIRES_NEW) is nog steeds gecommit
        assertThat(findAuditLog(orderId)).isNotNull();
        // Maar de bestelling is teruggedraaid
        assertThat(findOrder(orderId)).isNull();
    }

    @Test
    void mandatory_shouldThrowWithoutTransaction() {
        // Given - geen actieve transactie

        // When/Then - moet een exception gooien
        assertThatThrownBy(() -> auditService.logCriticalAction("TEST", 1L))
            .isInstanceOf(IllegalTransactionStateException.class)
            .hasMessageContaining("No existing transaction");
    }
}
PropagationIntegrationTest.javajava
// Integratietest met echte rollback en 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-verwerking met NESTED
        BatchResult result = orderService.processBatchWithNested(
            List.of(validItem(), invalidItem(), validItem())
        );

        // Then - alleen geldige items worden bewaard
        assertThat(result.getSuccessCount()).isEqualTo(2);
        assertThat(result.getFailureCount()).isEqualTo(1);
        assertThat(orderRepository.findAll().size()).isEqualTo(initialCount + 2);
    }
}

Vraag 12: Wat zijn de best practices voor transactieconfiguratie?

Een consistente transactionele configuratie op projectniveau voorkomt verrassingen en vereenvoudigt onderhoud.

TransactionConfig.javajava
// Gecentraliseerde transactieconfiguratie
@Configuration
@EnableTransactionManagement
public class TransactionConfig {

    // Standaard timeout voor alle transacties
    @Bean
    public PlatformTransactionManager transactionManager(DataSource dataSource) {
        DataSourceTransactionManager tm = new DataSourceTransactionManager(dataSource);
        tm.setDefaultTimeout(30); // 30 seconden max per transactie
        return tm;
    }
}

// BaseService.java
// Standaard annotaties voor services
@Service
@Transactional(
    readOnly = true, // Alleen-lezen standaard
    rollbackFor = Exception.class // Rollback bij elke exception
)
public abstract class BaseService {
    // Leesmethoden erven readOnly = true
}

// OrderService.java
// Service met consistente configuratie
@Service
public class OrderService extends BaseService {

    private final OrderRepository orderRepository;

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

    // Override voor schrijfoperaties
    @Transactional(readOnly = false)
    public Order createOrder(OrderRequest request) {
        return orderRepository.save(new Order(request));
    }

    // Expliciete configuratie voor kritieke gevallen
    @Transactional(
        readOnly = false,
        propagation = Propagation.REQUIRES_NEW,
        isolation = Isolation.SERIALIZABLE,
        timeout = 10
    )
    public void criticalOperation(Long orderId) {
        // Operatie met maximale isolatie en korte timeout
    }
}

Best practices checklist:

text
Transactieconfiguratie - Checklist

✅ readOnly = true standaard, expliciete override voor schrijfoperaties
✅ rollbackFor = Exception.class om checked exceptions op te nemen
✅ Passende timeout op basis van operatietype
✅ Vermijd interne aanroepen (@Transactional genegeerd)
✅ REQUIRES_NEW alleen wanneer onafhankelijke commit nodig is
✅ MANDATORY om transactionele context te garanderen
✅ Expliciete tests voor rollback-gedragingen
✅ Monitor langlopende transacties
✅ Documenteer niet-standaard propagatiekeuzes

Klaar om je Spring Boot gesprekken te halen?

Oefen met onze interactieve simulatoren, flashcards en technische tests.

Conclusie

Transactiepropagatie is een fundamenteel concept dat wordt geëvalueerd in Spring Boot sollicitatiegesprekken. Belangrijke punten om te onthouden:

Veelvoorkomende propagatietypen:

  • ✅ REQUIRED (standaard): sluit aan bij of maakt een transactie
  • ✅ REQUIRES_NEW: onafhankelijke transactie, aparte commit
  • ✅ NESTED: savepoint voor gedeeltelijke rollback
  • ✅ MANDATORY: vereist een bestaande transactie

Te vermijden valkuilen:

  • ✅ Self-invocation: omzeilt proxy, @Transactional genegeerd
  • ✅ Checked exceptions: geen rollback standaard
  • ✅ REQUIRES_NEW op dezelfde data: deadlock-risico
  • ✅ Ontbrekende timeout: transacties oneindig geblokkeerd

Best practices:

  • ✅ readOnly = true standaard
  • ✅ rollbackFor = Exception.class systematisch
  • ✅ Aparte services om self-invocation te vermijden
  • ✅ Test rollback-gedragingen expliciet

Transactiepropagatie beheersen toont diepgaand begrip van Spring en datamanagement. Deze concepten zijn essentieel om robuuste applicaties te ontwerpen en technische sollicitatiegesprekken succesvol te doorstaan.

Begin met oefenen!

Test je kennis met onze gespreksimulatoren en technische tests.

Tags

#spring boot
#transactions
#propagation
#java
#interview

Delen

Gerelateerde artikelen