Skip to content
Development
Skill

/transactional-patterns

Use when defining or debugging Spring Boot 3 database transaction boundaries, rollback, propagation, optimistic-lock retries, or commit-bound events. For duplicate HTTP commands use idempotency-patterns; for reactive transactions use webflux-reactive-patterns.

From plugin
spring-boot-skills
33733 skills

Claude Code

$ npx -y skills add rrezartprebreza/spring-boot-skills --skill transactional-patterns --agent claude-code

SKILL.md

transactional-patterns.SKILL.md
name: transactional-patterns
description: >
  Use when defining or debugging Spring Boot 3 database transaction boundaries, rollback,
  propagation, optimistic-lock retries, or commit-bound events. For duplicate HTTP
  commands use idempotency-patterns; for reactive transactions use webflux-reactive-patterns.

Transactional Patterns

Establish the boundary

  • Inspect the project's Boot/Framework version, transaction manager, persistence technology,

rollback configuration and caller before changing propagation.

  • Place multi-step business transactions at the application/service boundary. Preserve

Spring Data repository transactions; repository annotations are not inherently wrong.

  • Default propagation is `REQUIRED` — joins existing transaction or creates one
  • Use `readOnly = true` for appropriate read units of work. It is an optimization hint,

not write prevention, authorization or automatic routing to a read replica.

  • Keep existing architecture and transaction-manager choices. A local database transaction

does not make writes to another database or an HTTP provider atomic.

The snippets below are illustrative; domain classes and imports come from the application. Adapt the [service template](templates/TransactionalOrderService.java) and compare the [good](examples/good-transactional-service.java) and [bad](examples/bad-transactional-service.java) examples when editing a service.

@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

| 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 |

Participating `REQUIRED` calls use the outer transaction's isolation, timeout and read-only settings. Catching an inner failure does not clear its rollback-only marker; outer commit may still throw `UnexpectedRollbackException`.

`REQUIRES_NEW` needs another connection while the outer transaction retains its resources. Account for pool capacity and lock contention. Reserve independent commits for records that must survive a failed operation (for example, an attempted action), not a success audit row that claims a rolled-back order was created. Do not reference an uncommitted parent row from an independent audit transaction.

// 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
}

// Illustrative: record an attempt for an existing order; failures are unchecked.
@Transactional
public void processOrder(Order order) {
    auditService.logAuditEvent(new AuditEvent("ORDER_START", order.getId()));
    try {
        // ... process, may throw
    } catch (RuntimeException e) {
        auditService.logAuditEvent(new AuditEvent("ORDER_FAILED", order.getId()));
        throw e; // parent TX rolls back, audit TX already committed
    }
}

Self-Invocation Pitfall

// 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 — extract the independently transactional operation to another 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
    }
}

Handling Exceptions

// Default: RuntimeException and Error roll back; checked exceptions do not.
// Check project-wide rollback configuration before adding per-method rules.

@Transactional(rollbackFor = InsufficientInventoryException.class) // checked exception
public Order createOrder(CreateOrderRequest request) throws InsufficientInventoryException {
    ...
}

Never use `noRollbackFor` to recover from `OptimisticLockException`: the persistence provider marks the transaction rollback-only. Roll it back and retry the whole unit of work in a fresh transaction, with a bounded policy and a fresh entity read.

Optimistic Locking

@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); // conflict can surface at flush or commit
    return orderRepository.save(order);
}

Use the [version-specific retry guidance](../resilience-retry/SKILL.md) for dependencies and annotation imports. A retrying facade must not already hold a transaction that each attempt's `REQUIRED` method would join.

Distributed Transactions (Saga Pattern)

A saga coordinates durable local steps; wrapping remote calls in `@Transactional` does not implement one.

1. Commit the pending order, saga state and outgoing command in one local tra

Read more
@rrezartprebreza
Ships withspring-boot-skills

Production-grade Claude Code and Codex skills for Spring Boot developers

Stars on GitHub

Momentum

stars in total
337
Star history of rrezartprebreza/spring-boot-skills on GitHub, a chart by star-history.com

Other skills on spring-boot-skills.

More skills from other plugins.

brainstorming
Auto-invoked

brainstorming

You MUST use this before any creative work - creating features, building components, adding functionality, modifying behavior, or…

@obra@obraInstall Skill
ask-matt

ask-matt

Ask which skill or flow fits your situation. A router over the skills in this repo.

skills10d309k
@mattpocock@mattpocockInstall Skill
accessibility
Auto-invoked

accessibility

Design, implement, and audit accessible UI to WCAG 2.2 Level AA across Web, iOS, and Android — semantic ARIA roles and labels,…

ecc12d293k
@affaan-m@affaan-mInstall Skill
compiler-commit

compiler-commit

Use when you want to verify compiler changes and commit with the correct convention. Runs tests, lint, and format, then commits…

react1d252k
@facebook@facebookInstall Skill

Questions about transactional-patterns.

Which coding agents can use transactional-patterns?
transactional-patterns installs in Claude Code, Codex, Cursor, Gemini CLI, GitHub Copilot, OpenCode and Windsurf with the skills CLI, using each agent's flag shown on this page. Flowy's auto-invocation, which fires a skill when your prompt matches, runs in Claude Code only.