transactional-patterns — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited transactional-patterns (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
@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 intercept~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.