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()).
What a pipeline looks like
Section titled “What a pipeline looks like”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 responseThat’s the entire handler logic. The compiler enforces the threading model.
Pipeline method groups
Section titled “Pipeline method groups”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.
map — transform on the event loop
Section titled “map — transform on the event loop”Runs on the event loop with full access to application state.
| Method | Error handling |
|---|---|
map | Transform and continue |
flatMap | Return HttpResult.error() to short-circuit with a specific error response |
blocking — run on a worker thread
Section titled “blocking — run on a worker thread”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.
| Method | Error handling |
|---|---|
blockingMap | Transform and continue |
blockingFlatMap | Return HttpResult.error() to short-circuit with a specific error response |
async — start async work
Section titled “async — start async work”Kicks off asynchronous operations (HTTP calls to other services, Kafka publishes) and returns a future.
| Method | Thread | Error handling |
|---|---|---|
asyncMap | Event loop | Transform and continue |
asyncFlatMap | Event loop | Can short-circuit |
asyncBlockingMap | Worker | Transform and continue |
asyncBlockingFlatMap | Worker | Can short-circuit |
terminal — end the pipeline
Section titled “terminal — end the pipeline”| Method | Thread |
|---|---|
complete | Event loop |
blockingComplete | Worker |
validate — declarative field validation
Section titled “validate — declarative field validation”validate runs on the event loop and short-circuits with a 422 response if any rules fail. See Validation for details.
The flat pattern
Section titled “The flat pattern”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.
Context access
Section titled “Context access”| Thread | Available 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.