Spring Integration

The fun-spring module adds first-class Spring transaction support for Result<V,E>, Try<V>, and Validated<E,A>. It is an optional dependency — the core fun library has no runtime dependency on Spring.

The problem

Spring’s @Transactional rolls back only when an unchecked exception escapes the annotated method. Result, Try, and Validated capture failure as return values, so no exception escapes — and the transaction commits even when the operation failed, silently persisting partial writes.

TxResult, TxTry, and TxValidated solve this by inspecting the return value: an error/failure/invalid outcome marks the transaction rollback-only before Spring commits.

Adding the dependency

fun-spring declares spring-tx and spring-context as compileOnly. You must add them to your own classpath — typically they arrive transitively via spring-webmvc or spring-boot-starter-data-jpa. Spring Framework releases 6.0.x, 6.1.x, 6.2.x, and 7.0.x are supported (see version compatibility below).

<!-- Maven -->
<dependency>
<groupId>codes.domix</groupId>
<artifactId>fun-spring</artifactId>
<version>LATEST_VERSION</version>
</dependency>
// Gradle
implementation("codes.domix:fun-spring:LATEST_VERSION")

Setup

Both components are Spring @Component beans. You need one PlatformTransactionManager in your context — Spring Boot auto-configures one for every registered DataSource.

Spring Boot — no extra wiring needed: component scanning picks up TxResult, TxTry, and TxValidated automatically as long as fun-spring is on the classpath.

Plain Spring — declare the beans explicitly or add the dmx.fun.spring package to your @ComponentScan.

// Plain Spring — explicit registration (only needed without component scanning)
@Bean public TxResult txResult(PlatformTransactionManager tm) { return new TxResult(tm); }
@Bean public TxTry txTry(PlatformTransactionManager tm) { return new TxTry(tm); }
@Bean public TxValidated txValidated(PlatformTransactionManager tm) { return new TxValidated(tm); }

TxResult

Wraps a Result<V,E>-returning action in a managed transaction. Rolls back when the result is isError().

@Service
public class OrderService {
private final TxResult tx;
private final OrderRepository repo;
private final InventoryClient inventory;
public OrderService(TxResult tx, OrderRepository repo, InventoryClient inventory) {
this.tx = tx; this.repo = repo; this.inventory = inventory;
}
public Result<Order, OrderError> createOrder(OrderRequest req) {
return tx.execute(() ->
validate(req) // Result<ValidOrder, OrderError>
.flatMap(repo::save) // Result<Order, OrderError>
.flatMap(inventory::reserve) // Result<Order, OrderError>
);
// • Ok(order) → transaction commits
// • Err(orderError) → transaction rolls back; partial writes undone
}
}

Rollback contract:

OutcomeTransaction
Action returns Result.isOk()Commits
Action returns Result.isError()Rolls back
Action throws an unchecked exceptionRolls back, rethrows

TxTry

Wraps a Try<V>-returning action in a managed transaction. Rolls back when the result is isFailure().

@Service
public class ReportService {
private final TxTry tx;
private final ReportRepository repo;
private final AuditLog audit;
public ReportService(TxTry tx, ReportRepository repo, AuditLog audit) {
this.tx = tx; this.repo = repo; this.audit = audit;
}
public Try<Report> generate(ReportRequest req) {
return tx.execute(() ->
Try.of(() -> repo.save(build(req))) // Try<Report>
.map(r -> { audit.record(r); return r; }) // Try<Report>
);
// • Success(report) → transaction commits
// • Failure(cause) → transaction rolls back; partial writes undone
}
}

Rollback contract:

OutcomeTransaction
Action returns Try.isSuccess()Commits
Action returns Try.isFailure()Rolls back
Action throws an unchecked exceptionRolls back, rethrows

TxValidated

Wraps a Validated<E,A>-returning action in a managed transaction. Rolls back when the result is isInvalid().

@Service
public class RegistrationService {
private final TxValidated tx;
private final UserRepository repo;
public RegistrationService(TxValidated tx, UserRepository repo) {
this.tx = tx; this.repo = repo;
}
public Validated<NonEmptyList<String>, User> register(RegistrationRequest req) {
return tx.execute(() ->
validateName(req)
.combine(validateEmail(req), (name, email) -> new UserDraft(name, email))
.map(repo::save)
);
// • Valid(user) → transaction commits
// • Invalid(errors) → transaction rolls back; no partial writes persisted
}
}

Rollback contract:

OutcomeTransaction
Action returns Validated.isValid()Commits
Action returns Validated.isInvalid()Rolls back
Action throws an unchecked exceptionRolls back, rethrows

Custom TransactionDefinition

All three components accept an explicit TransactionDefinition to control propagation, isolation level, timeout, and read-only flag:

// Read-only transaction — hint to the JDBC driver and database optimizer
var readOnly = new DefaultTransactionDefinition();
readOnly.setReadOnly(true);
Result<List<Order>, String> orders = tx.execute(readOnly, orderRepo::findAll);
// Explicit isolation level — e.g. SERIALIZABLE for critical financial operations
var serializable = new DefaultTransactionDefinition();
serializable.setIsolationLevel(TransactionDefinition.ISOLATION_SERIALIZABLE);
Result<Transfer, TransferError> transfer = tx.execute(serializable, () ->
debit(source, amount).flatMap(__ -> credit(target, amount))
);
// Custom timeout — rolls back automatically if the action exceeds 5 seconds
var withTimeout = new DefaultTransactionDefinition();
withTimeout.setTimeout(5);
Result<Report, String> report = tx.execute(withTimeout, () ->
Result.ok(reportEngine.generate(params))
);

TxResult vs TxTry vs @Transactional

// ---- @Transactional — commits even when Result is an error ----
@Transactional
public Result<Order, OrderError> createOrder(OrderRequest req) {
return validate(req)
.flatMap(repo::save);
// ❌ If flatMap returns Err(orderError), the method returns normally —
// no exception escapes, so Spring commits the transaction.
// In a longer pipeline (preAuth → save → ledger.record), writes
// from steps that succeeded before the Err are silently persisted.
}
// ---- TxResult — rolls back when Result is an error ----
public Result<Order, OrderError> createOrder(OrderRequest req) {
return tx.execute(() ->
validate(req)
.flatMap(repo::save)
);
// ✓ If flatMap returns Err(orderError), TxResult marks the transaction
// rollback-only before Spring commits. Partial writes are undone.
}
// ---- When @Transactional is still the right choice ----
@Transactional
public void deleteOrder(String id) {
// No return value, rollback happens naturally on RuntimeException.
// @Transactional is simpler and perfectly correct here.
repo.delete(id);
}

Rule of thumb:

  • Use TxResult when your pipeline returns Result<V,E> for typed domain errors.
  • Use TxTry when your pipeline uses Try<V> to wrap exception-throwing code.
  • Use TxValidated when your pipeline accumulates multiple validation errors and you want atomicity across all steps.
  • Use @Transactional when the method has no dmx-fun return type and rollback on unchecked exception is sufficient (void methods, simple queries).

Version compatibility

The module is tested in CI against the following Spring Framework versions on every pull request that touches the spring/ module:

Spring versionStatus
6.0.xtested
6.1.xtested
6.2.xtested
7.0.xtested

To test locally against a specific version:

Terminal window
./gradlew :spring:test -PspringVersion=7.0.6