Use when data appears inconsistent after failures, when two writes need to succeed or fail together, or when transactions are causing lock contention or timeout errors under load. Reviews isolation levels, atomicity gaps, overly wide...
Use when:
@Transactional boundaries in Spring Boot codeDo not use when:
sql-pro)connection-pool-tuner)@Transactional? Does it include external I/O?For full Outbox and Saga implementations, see references/distributed-patterns.md.
For each review, provide:
| Claude | You |
|---|---|
Identifies anti-patterns in the @Transactional scope | Provide the service method and repository code |
| Recommends isolation level for the specific anomaly | Confirm concurrent access patterns under real load |
| Generates corrected code with narrowed transaction scope | Test the fix under concurrent load |
| Designs Outbox table and relay pattern | Implement and monitor the outbox relay process |
| Templates Saga step with execute() and compensate() | Fill in the real compensation business logic |
| Property | Means | Violated By |
|---|---|---|
| Atomicity | All changes commit or all roll back | Partial writes on failure |
| Consistency | DB constraints hold before and after | Bypassing validations; wrong ordering |
| Isolation | Concurrent transactions do not interfere | Missing locks; wrong isolation level |
| Durability | Committed data survives crashes | Missing fsync; premature ack |
| Level | Prevents | Allows | Use When |
|---|---|---|---|
| READ UNCOMMITTED | Nothing | Dirty reads, non-repeatable reads, phantoms | Almost never |
| READ COMMITTED | Dirty reads | Non-repeatable reads, phantoms | Default OLTP (PostgreSQL default) |
| REPEATABLE READ | Dirty + non-repeatable reads | Phantom reads | Financial aggregations, inventory checks |
| SERIALIZABLE | All anomalies | — | Booking, reservation, double-spend prevention |
// Bad — HTTP call inside transaction holds locks
@Transactional
public void processOrder(Order order) {
orderRepository.save(order);
paymentGateway.charge(order); // external HTTP — locks held during this!
}
// Good — database work is atomic; side effects happen after commit
@Transactional
public Order saveOrder(Order order) { return orderRepository.save(order); }
public void processOrder(Order order) {
Order saved = saveOrder(order); // transaction commits here
paymentGateway.charge(saved); // no locks held
}
// Bad — IOException does NOT trigger rollback in Spring by default
@Transactional
public void importData(File file) throws IOException { ... }
// Good — explicit rollback declaration
@Transactional(rollbackFor = IOException.class)
public void importData(File file) throws IOException { ... }
// Bad — two threads read balance=100, both deduct 80, both save 20
@Transactional
public void deduct(Long accountId, BigDecimal amount) {
Account account = accountRepo.findById(accountId).orElseThrow();
account.setBalance(account.getBalance().subtract(amount));
accountRepo.save(account);
}
// Good — pessimistic lock (SELECT FOR UPDATE)
@Transactional
public void deduct(Long accountId, BigDecimal amount) {
Account account = accountRepo.findByIdWithLock(accountId).orElseThrow();
account.setBalance(account.getBalance().subtract(amount));
accountRepo.save(account);
}
| Pattern | When to Use |
|---|---|
| Outbox | Publishing events reliably after a local commit |
| Saga (choreography) | Long-running processes; 2–3 services |
| Saga (orchestration) | Complex multi-step flows needing visibility; 4+ services |
| Two-Phase Commit | Avoid; only when strong consistency is non-negotiable and you control both systems |
@Transactional rollbackFor covers checked exceptions that should rollbackspring-boot-engineer — for implementing the corrected @Transactional patternsmicroservices-architect — when the issue is cross-service transaction designcircuit-breaker-tuner — wide transactions compound cascading failure riskconnection-pool-tuner — wide transactions can exhaust the connection pool