Skip to content

Handler Pipeline

Handlers are typed transformation chains built by calling methods on HttpStream. Each step receives a context object (ctx) that gives you access to the current value (ctx.in()), the session (ctx.session()), and — on the event loop — the application state (ctx.app()).

This pipeline validates input, updates application state, checks a database, writes to it, fetches data from another service, publishes a Kafka event, and produces a response. Each step declares exactly where it runs:

return stream
.validate(v -> {
v.field("name", r -> r.name).required().minLength(2);
v.field("email", r -> r.email).required().email();
})
.map(this::updateApplicationState) // event loop — access app state
.blockingFlatMap(this::validateAgainstDatabase) // worker thread — can handle failure case
.blockingMap(this::writeToDatabase) // worker thread — blocking I/O
.asyncMap(this::fetchFromUserService) // event loop — async HTTP call and handle response
.asyncMap(this::sendKafkaEvent) // event loop — async Kafka publish and handle a correlated response with luxis.handleAsyncResponse() else where
.peek(this::emitMetrics) // event loop — no transformation, truly async, don't expect a response
.flatMap(this::handleKafkaResponse) // event loop — can handle failure case
.complete(this::toResponse); // event loop — produce response

That’s the entire handler logic. The compiler enforces the threading model.

Every pipeline method is a combination of three concepts: where it runs, whether it’s async, and whether it handles error cases explicitly.

Any step can still throw an exception — Luxis will catch it and return a 500. The flat variants give you control over the error response, letting you return a specific status code and message when your application logic dictates a failure.

Runs on the event loop with full access to application state.

MethodError handling
mapTransform and continue
flatMapReturn HttpResult.error() to short-circuit with a specific error response

Moves execution to a worker thread for synchronous I/O (database calls, file reads). Application state is not accessible — this is enforced by the compiler.

MethodError handling
blockingMapTransform and continue
blockingFlatMapReturn HttpResult.error() to short-circuit with a specific error response

Kicks off asynchronous operations (HTTP calls to other services, Kafka publishes) and returns a future.

MethodThreadError handling
asyncMapEvent loopTransform and continue
asyncFlatMapEvent loopCan short-circuit
asyncBlockingMapWorkerTransform and continue
asyncBlockingFlatMapWorkerCan short-circuit
MethodThread
completeEvent loop
blockingCompleteWorker

validate runs on the event loop and short-circuits with a 422 response if any rules fail. See Validation for details.

Any method with flat in the name lets you explicitly handle error cases and control the response:

.flatMap(ctx -> {
if (ctx.in().age() < 0) {
return HttpResult.error(ErrorStatusCode.BAD_REQUEST,
new ErrorMessageResponse("Age must be positive"));
}
return HttpResult.success(ctx.in());
})

Without flat, the step simply transforms the value and continues. If something unexpected goes wrong, Luxis catches the exception and returns a 500 — but flat lets you decide exactly what the client sees.

ThreadAvailable context
Event loop (map, flatMap, asyncMap, etc.)ctx.in(), ctx.session(), ctx.app()
Worker (blockingMap, blockingFlatMap, etc.)ctx.in(), ctx.session()

Application state (ctx.app()) is only available on the event loop. This isn’t a convention — it’s a compile-time constraint. The type system won’t let you access shared state from a worker thread.