Skip to content

Add support for Either besides Try in TransactionAspectSupport in spring-tx #36128

@emedina

Description

@emedina

Description

Spring Framework already supports Vavr's Try type for transaction rollback decisions in TransactionAspectSupport. This proposal requests adding equivalent support for Vavr's Either type, which is widely used for railway-oriented programming and functional error handling.

Motivation

Either<L, R> is a common pattern for representing success (Right) or failure (Left) without throwing exceptions. It's the preferred approach for:

  • Railway-oriented programming
  • Explicit error handling in functional pipelines
  • Domain-driven design where errors are domain concepts, not exceptions

Currently, a method like this will commit even when returning a failure:

@Transactional
public Either<PaymentError, Receipt> processPayment(PaymentRequest request) {
    return validateRequest(request)
        .flatMap(this::checkFunds)
        .flatMap(this::executeTransfer);
    // Returns Either.Left(InsufficientFunds) → transaction COMMITS (unexpected!)
}

Users expect Either.Left to trigger a rollback, similar to how Try.Failure does.

Proposed Change

Add Either support in TransactionAspectSupport.VavrDelegate, mirroring the existing Try implementation:

In invokeWithinTransaction() (~line 380):

// Existing Try support
else if (vavrPresent && VavrDelegate.isVavrTry(retVal)) {
    retVal = VavrDelegate.evaluateTryFailure(retVal, txAttr, status);
}
// Proposed Either support
else if (vavrPresent && VavrDelegate.isVavrEither(retVal)) {
    retVal = VavrDelegate.evaluateEitherLeft(retVal, txAttr, status);
}

In VavrDelegate inner class:

public static boolean isVavrEither(@Nullable Object retVal) {
    return (retVal instanceof Either<?, ?> either && either.isLeft());
}

public static Object evaluateEitherLeft(Object retVal,
        @Nullable TransactionAttribute txAttr, TransactionStatus status) {
    return ((Either<?, ?>) retVal).peekLeft(left -> {
        if (left instanceof Throwable throwable) {
            if (txAttr != null && txAttr.rollbackOn(throwable)) {
                status.setRollbackOnly();
            }
        } else {
            // Non-Throwable Left: rollback by default
            status.setRollbackOnly();
        }
    });
}

Behavior

Return Value Transaction Outcome
Either.Right(value) Commit
Either.Left(throwable) Rollback if txAttr.rollbackOn(throwable) matches
Either.Left(nonThrowable) Rollback (default)

Scope

This proposal (Phase 1):

  • Basic Either.Left → rollback support
  • Consistent with existing Try.Failure behavior
  • Minimal change (~30-50 lines)

Future enhancement (Phase 2):

  • Custom rollback rules for non-Throwable types (rollbackForWithEither attribute)
  • Fine-grained control over which Left values trigger rollback

Impact

  • No breaking changes - additive only
  • No new dependencies - Vavr is already optional
  • Consistent - matches existing Try support pattern
  • Low risk - isolated to VavrDelegate

Alternatives Considered

  1. Do nothing - Users must wrap Either in Try or throw exceptions (breaks functional style)
  2. Custom TransactionInterceptor - Requires significant boilerplate for each project
  3. AOP around advice - Happens after commit, too late for rollback

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    in: dataIssues in data modules (jdbc, orm, oxm, tx)status: waiting-for-triageAn issue we've not yet triaged or decided on

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions