Skip to content

Transactions

Pipelines can open a transaction around a sub-chain of steps. Luxis orchestrates the lifecycle — begin, commit, rollback, onCommitted — but stays out of the driver itself. You supply a DatabaseClient, and your async driver calls (JDBC via a worker, R2DBC, Vert.x SQL client) run inside the managed window.

stream
.map(ctx -> ctx.in().userId())
.inTransaction(tx -> tx
.asyncMap(ctx -> loadUser(ctx.in())) // Future<User>
.flatMap(ctx -> ctx.in().active()
? Result.success(ctx.in())
: HttpResult.error(ErrorStatusCode.CONFLICT,
new ErrorMessageResponse("user inactive")))
.asyncMap(ctx -> debitBalance(ctx.in(), 100))
.onCompletion(ctx -> ctx.app().recordDebit(ctx.in()))
.commit())
.complete(ctx -> HttpResult.success(new DebitResponse(ctx.in())));

Inside inTransaction(...):

  • map / flatMap — sync transforms on the event loop (same semantics as the outer pipeline).
  • asyncMap — returns Vert.x Future<T>. This is the one coupling point: your driver’s async calls adapt to Future.
  • peek — side effect without transformation.
  • onCompletion(ctx -> ...) — registers a post-commit callback. Fires via DatabaseClient.onCommitted; does not fire on rollback.
  • commit() — closes the sub-chain. Required.

Only these operations are available inside the sub-chain. Blocking steps and arbitrary HTTP / Kafka calls are deliberately excluded — they’d hold a DB connection open on uncontrolled I/O.

Luxis.app(routes)
.withConfig(config)
.withDatabase(new MyDatabaseClient())
.start();

DatabaseClient<TX, ROW, KEY> is the driver SPI. The transaction methods are:

public interface DatabaseClient<TX, ROW, KEY> {
Future<TX> begin();
Future<Void> commit(TX tx);
Future<Void> rollback(TX tx);
default Future<Void> onCommitted(TX tx, Runnable callback) {
callback.run();
return Future.succeededFuture();
}
// …query / update / updateBatch methods omitted
}

TX is whatever token your driver needs to identify a transaction — a connection, a session, a correlation id. Luxis hands it back to you verbatim on commit / rollback / onCommitted. ROW and KEY parameterise the row type and generated-key type for the query and update methods on the same interface.

Calling .inTransaction(...) on a route without a DatabaseClient registered fails at route registration time, not at request time.

Luxis threads the active TX through the transactional context. Inside inTransaction, ctx.db() returns a DbAccessor bound to the current transaction, so your asyncMap bodies can issue queries and updates through the driver without manually tracking the TX. The same DatabaseClient.begin / commit / rollback lifecycle Luxis drives is what makes that token available.

If you need raw access to the TX outside of the supplied accessor (for example, to call a driver-specific API), stash it from your DatabaseClient.begin() implementation onto a Vert.x context-local or a ScopedValue you manage, and clear it from commit / rollback. Keep that arrangement consistent with the thread your driver expects.

Inside a transaction, asyncMap returns io.vertx.core.Future<T>. Outside, the pipeline uses Luxis’s own async mappers. The inside-tx choice is deliberate: transactional drivers already expose Future (Vert.x SQL client) or adapt trivially (CompletionStage.toFuture() for R2DBC, worker-executed JDBC via vertx.executeBlocking(...)).

This is the one place the framework is coupled to Vert.x in the public API. Document this to callers.

Sub-chain outcomeLuxis callsonCompletion hooks
All steps succeedcommitonCommittedFire (via onCommitted)
Any step returns Result.errorrollbackDo not fire
Step throwsrollback, error bubbles to exceptionHandlerDo not fire
commit itself failserror bubbles to exceptionHandlerDo not fire
rollback itself failserror bubbles to exceptionHandler

onCompletion runs after commit resolves. If a hook throws, the exception goes to the configured exceptionHandler — the client still sees the success response, because by that point the transaction has already committed.

The outer pipeline continues as soon as commit succeeds. Luxis does not wait for onCommitted to resolve before producing the response. Drivers with native post-commit hooks (Postgres LISTEN/NOTIFY, MySQL binlog tailers) can override onCommitted to fire the callback from the driver’s own event rather than inline.