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>// Gradleimplementation("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().
@Servicepublic 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:
| Outcome | Transaction |
|---|---|
Action returns Result.isOk() | Commits |
Action returns Result.isError() | Rolls back |
| Action throws an unchecked exception | Rolls back, rethrows |
TxTry
Wraps a Try<V>-returning action in a managed transaction. Rolls back when the result
is isFailure().
@Servicepublic 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:
| Outcome | Transaction |
|---|---|
Action returns Try.isSuccess() | Commits |
Action returns Try.isFailure() | Rolls back |
| Action throws an unchecked exception | Rolls back, rethrows |
TxValidated
Wraps a Validated<E,A>-returning action in a managed transaction. Rolls back when the
result is isInvalid().
@Servicepublic 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:
| Outcome | Transaction |
|---|---|
Action returns Validated.isValid() | Commits |
Action returns Validated.isInvalid() | Rolls back |
| Action throws an unchecked exception | Rolls 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 optimizervar 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 operationsvar 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 secondsvar 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 ----
@Transactionalpublic 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 ----
@Transactionalpublic 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
TxResultwhen your pipeline returnsResult<V,E>for typed domain errors. - Use
TxTrywhen your pipeline usesTry<V>to wrap exception-throwing code. - Use
TxValidatedwhen your pipeline accumulates multiple validation errors and you want atomicity across all steps. - Use
@Transactionalwhen 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 version | Status |
|---|---|
| 6.0.x | tested |
| 6.1.x | tested |
| 6.2.x | tested |
| 7.0.x | tested |
To test locally against a specific version:
./gradlew :spring:test -PspringVersion=7.0.6