Skip to content

Introduce non-null transaction callback/result option for TransactionOperations (@NullMarked / JSpecify ergonomics) #36655

@gysi

Description

@gysi

Note: AI-assisted wording/structure; problem statement, proposal are my own.

Summary

I’d like to discuss improving null-safety ergonomics for programmatic transactions in spring-tx.

TransactionOperations#execute(TransactionCallback<T>) is correctly @Nullable by contract, since callbacks may return null. In @NullMarked / JSpecify codebases, this propagates nullable types into many call sites, including flows where null is not a valid business outcome.

Context / real-world problem

While refactoring production service code to @NullMarked, we repeatedly ended up with patterns like:

var result = Objects.requireNonNull(
    transactionTemplate.execute(status -> repository.merge(entity)),
    "Transactional callback returned null; this should never happen"
);

This is valid, but repetitive. We introduced an internal wrapper (TransactionalExecutor) + dedicated non-null callback type to keep call sites explicit and reduce repeated null guards.

Proposal direction

I’d like to discuss whether Spring should offer an official non-null path for this common case.

One possible shape:

<T> T execute(NonNullTransactionCallback<T> action) throws TransactionException;

Another possible shape is a distinct method name (for example executeRequired(...)) that enforces non-null results.

Naming in this issue (execute, executeRequired, NonNullTransactionCallback) is illustrative only and fully open for discussion.

Important API caveat (with minimal repro)

If a non-null callback variant is added as an overload next to existing execute(TransactionCallback<T>), lambda calls can become ambiguous because both callback types are SAM interfaces with the same shape.

Minimal example:

interface Test {
    void test(TestCallback cb);
    void test(TestCallback2 cb);
}

interface TestCallback { void test(); }
interface TestCallback2 { void test(); }

new Implementation().test(() -> System.out.println("Test")); // ambiguous

Then callers must cast:

new Implementation().test((TestCallback) () -> System.out.println("Test"));

I’m not sure this would be acceptable ergonomically for Spring users, so I wanted to raise this explicitly in the design discussion.

Why framework-level support could still help

  • Common framework-driven pain point in strict nullness code.
  • Reduces repetitive requireNonNull(...) boilerplate.
  • Makes intent explicit at call sites.
  • Encourages a consistent idiom across projects.

Questions for maintainers

  1. Is a first-class non-null transaction execution path aligned with Spring transaction API design?
  2. Given lambda ambiguity risk, what API shape would you prefer?
    • overload with dedicated callback type,
    • distinct method name,
    • or another approach.
  3. If runtime enforcement is used, what exception semantics/message would be preferred when null is returned unexpectedly?

If this direction is useful, I’m happy to prepare a PR.

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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions