> Use when working with @Transactional, multi-step database operations, distributed transactions, or any code that needs atomicity guarantees. Covers propagation rules, isolation levels, read-only optimization, and common pitfalls.
npx skills add https://github.com/rrezartprebreza/spring-boot-skills --skill transactional-patterns
@Transactional belongs on service methods, never controllers or repositoriesREQUIRED — joins existing transaction or creates one@Transactional(readOnly = true) on all read-only service methods — enables optimizations@Service
@RequiredArgsConstructor
@Transactional(readOnly = true) // default for all methods in this service
public class OrderService {
@Transactional // overrides readOnly for writes
public Order createOrder(CreateOrderRequest request) {
inventoryService.reserve(request.items()); // participates in same TX
return orderRepository.save(Order.from(request));
}
public Optional<Order> findById(UUID id) {
return orderRepository.findById(id); // readOnly = true inherited
}
}
| Propagation | Behavior |
|-------------|----------|
| REQUIRED (default) | Join existing TX or create new |
| REQUIRES_NEW | Always create new TX, suspend existing |
| SUPPORTS | Join if exists, proceed without TX if not |
| NOT_SUPPORTED | Always run without TX |
| MANDATORY | Must have existing TX, throw if not |
| NEVER | Must NOT have TX, throw if one exists |
// REQUIRES_NEW — for audit logging that must survive rollback
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void logAuditEvent(AuditEvent event) {
auditRepository.save(event); // commits independently of parent TX
}
// Order TX rolls back, audit log still saved
@Transactional
public void processOrder(Order order) {
auditService.logAuditEvent(new AuditEvent("ORDER_START", order.getId()));
try {
// ... process, may throw
} catch (Exception e) {
auditService.logAuditEvent(new AuditEvent("ORDER_FAILED", order.getId()));
throw e; // parent TX rolls back, audit TX already committed
}
}
// ❌ BROKEN — self-invocation bypasses Spring proxy, @Transactional ignored
@Service
public class OrderService {
@Transactional
public void processAll(List<UUID> ids) {
ids.forEach(id -> this.processSingle(id)); // bypasses proxy!
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void processSingle(UUID id) { ... } // never creates new TX
}
// ✅ FIX — inject self or extract to separate bean
@Service
@RequiredArgsConstructor
public class OrderService {
private final OrderProcessor orderProcessor; // separate bean
@Transactional
public void processAll(List<UUID> ids) {
ids.forEach(id -> orderProcessor.processSingle(id)); // goes through proxy
}
}
// @Transactional rolls back on RuntimeException by default
// For checked exceptions, explicitly declare rollbackFor
@Transactional(rollbackFor = InsufficientInventoryException.class) // checked exception
public Order createOrder(CreateOrderRequest request) throws InsufficientInventoryException {
...
}
// noRollbackFor — for non-fatal exceptions you want to commit anyway
@Transactional(noRollbackFor = OptimisticLockException.class)
public void updateWithRetry(UUID id) { ... }
@Entity
public class Order {
@Version
private Long version; // Hibernate handles conflicts automatically
}
// Handles concurrent updates
@Transactional
public Order updateStatus(UUID id, OrderStatus newStatus) {
Order order = orderRepository.findById(id).orElseThrow();
order.updateStatus(newStatus); // if another TX modified it, throws ObjectOptimisticLockingFailureException
return orderRepository.save(order);
}
For multi-service operations, use the Saga pattern instead of distributed TX:
@Service
@RequiredArgsConstructor
public class OrderSaga {
@Transactional
public void execute(CreateOrderRequest request) {
Order order = orderRepository.save(Order.create(request));
try {
inventoryClient.reserve(request.items()); // step 1
paymentClient.charge(order.getId(), request.total()); // step 2
order.confirm();
orderRepository.save(order);
} catch (PaymentException e) {
inventoryClient.release(request.items()); // compensate step 1
order.fail("Payment failed");
orderRepository.save(order);
throw e;
}
}
}
Never fire an external side effect (email, Kafka publish, webhook, cache warm) inside the transaction —
if the TX rolls back, you've already sent it. Bind the side effect to the commit instead:
// Publisher — inside the TX
@Transactional
public Order place(UUID id) {
Order order = orderRepository.findById(id).orElseThrow();
order.place();
eventPublisher.publishEvent(new OrderPlaced(order.getId())); // not sent yet
return orderRepository.save(order);
}
// Listener — runs ONLY if the TX commits successfully
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void onOrderPlaced(OrderPlaced event) {
emailService.sendConfirmation(event.orderId()); // safe: data is durable
}
AFTER_COMMIT runs after the DB commits. Note: it runs outside the original transaction, so a
new @Transactional(REQUIRES_NEW) is needed if the listener itself writes to the DB. This is the
clean way to publish the domain events collected in the [[domain-driven-design]] aggregate.
@Transactional on controllers — only on service layer@TransactionalEventListener(AFTER_COMMIT)readOnly = true on read methods — missed DB optimization@Transactional methods on this — self-invocation bypasses proxyrollbackFor@Transactional on private methods — Spring proxy can't interceptEfficient database search tool for bioRxiv preprint server. Use this skill when searching for life sciences preprints by keywords, authors, date ranges, or categories, retrieving paper metadata, downloading PDFs, or conducting literature reviews.
Access BRENDA enzyme database via SOAP API. Retrieve kinetic parameters (Km, kcat), reaction equations, organism data, and substrate-specific enzyme information for biochemical research and metabolic pathway analysis.
Access ClinPGx pharmacogenomics data (successor to PharmGKB). Query gene-drug interactions, CPIC guidelines, allele functions, for precision medicine and genotype-guided dosing decisions.
Query NCBI ClinVar for variant clinical significance. Search by gene/position, interpret pathogenicity classifications, access via E-utilities API or FTP, annotate VCFs, for genomic medicine.
Access COSMIC cancer mutation database. Query somatic mutations, Cancer Gene Census, mutational signatures, gene fusions, for cancer research and precision oncology. Requires authentication.
Query Ensembl genome database REST API for 250+ species. Gene lookups, sequence retrieval, variant analysis, comparative genomics, orthologs, VEP predictions, for genomic research.
Query openFDA API for drugs, devices, adverse events, recalls, regulatory submissions (510k, PMA), substance identification (UNII), for FDA regulatory data analysis and safety research.
Query NCBI Gene via E-utilities/Datasets API. Search by symbol/ID, retrieve gene info (RefSeqs, GO, locations, phenotypes), batch lookups, for gene annotation and functional analysis.
Take rrezartprebreza/transactional-patterns from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.