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.xFuture<T>. This is the one coupling point: your driver’s async calls adapt toFuture.peek— side effect without transformation.onCompletion(ctx -> ...)— registers a post-commit callback. Fires viaDatabaseClient.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.
Registering a DatabaseClient
Section titled “Registering a DatabaseClient”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.
TX access inside the sub-chain
Section titled “TX access inside the sub-chain”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.
Future<T> is the async primitive
Section titled “Future<T> is the async primitive”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.
Error semantics
Section titled “Error semantics”| Sub-chain outcome | Luxis calls | onCompletion hooks |
|---|---|---|
| All steps succeed | commit → onCommitted | Fire (via onCommitted) |
Any step returns Result.error | rollback | Do not fire |
| Step throws | rollback, error bubbles to exceptionHandler | Do not fire |
commit itself fails | error bubbles to exceptionHandler | Do not fire |
rollback itself fails | error 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.