Build Your First Reactive Messaging App in Quarkus
From REST to Real-Time: A Hands-On Tutorial for Java Developers
Modern systems rarely work in isolation. Orders stream in from mobile apps. Notifications go out to customers. Services talk to each other even when nobody is waiting on the other end. The request/response model we grew up with still matters, but it is no longer the default for high-throughput, real-time workloads.
This article introduces MicroProfile Reactive Messaging through a complete, end-to-end hands-on example in Quarkus. You’ll build a Food Delivery pipeline that simulates opening a “tap”, accepting orders via REST, transforming them in a kitchen service, then aggregating all prepared orders and sending a final notification when the tap closes.
This example teaches you the mechanics behind asynchronous messaging, completion signals, channel wiring, and message flow—without running Kafka or AMQP. Perfect for beginners, powerful enough to build on.
Why Event-Driven Matters
REST works best when:
the caller waits for a quick response
the operation is short-lived
both services depend on each other
But many workflows don’t follow that pattern. A food delivery order triggers kitchen preparation, driver assignment, and customer notifications. None of these should block the customer’s request.
Event-driven architectures improve:
Latency by avoiding blocking waits
Throughput by decoupling producers and consumers
Resilience by isolating failures
Flexibility by enabling fan-out and streaming
Reactive Messaging sits in the middle ground: simple Java methods annotated with @Incoming and @Outgoing, connected by channels. No ceremony. No framework learning curve.
Core Concepts You Need to Know
Before writing code, understand the building blocks:
Channels
Named streams of messages: “orders”, “kitchen”, “totals”.
@Outgoing
A method produces messages into a channel.
@Incoming
A method consumes messages from a channel.
Connectors
They translate channels into actual transport layers
(smallrye-kafka, smallrye-amqp, smallrye-in-memory, etc.)
Completion
Streams can complete, and downstream processors can react to that event.
These pieces form a graph that Quarkus wires together at startup. If you want another way of thinking about this, read:
What You Will Build Today
A realistic, multi-stage pipeline:
open tap → receive orders via REST
→ kitchen service transforms orders
→ aggregator sums prices when tap closes
→ final notification is sentThis flow demonstrates:
individual message emission
multi-stage message transformations
end-of-stream behavior
stateful aggregation
final message emission after completion
Let’s build it step by step.
Project Setup
Create a fresh Quarkus project with the Quarkus cli or grab the code from my Github repository!
quarkus create app com.acme:food-delivery \
--extensions="quarkus-messaging,rest" \
--no-code
cd food-deliveryDomain Model
Create two simple record types.
Order
package com.acme.model;
public record Order(String id, String item, double price) {}
KitchenTicket
package com.acme.model;
public record KitchenTicket(String orderId, String item, String status, double price) {}These are our message payloads. No extra metadata needed.
Controlling the Tap: Order Gateway
The Order Gateway exposes two operations:
open the tap
close the tap
send orders via REST only when the tap is open
When closing the tap, it sends a completion signal downstream.
package com.acme;
import org.eclipse.microprofile.reactive.messaging.Channel;
import org.eclipse.microprofile.reactive.messaging.Emitter;
import com.acme.model.Order;
import io.smallrye.mutiny.Uni;
import jakarta.enterprise.context.ApplicationScoped;
@ApplicationScoped
public class OrderGateway {
@Channel(”orders”)
Emitter<Order> orderEmitter;
private volatile boolean tapOpen = false;
public void openTap() {
tapOpen = true;
}
public void closeTap() {
tapOpen = false;
orderEmitter.complete(); // important: closes the stream
}
public Uni<Void> submitOrder(Order order) {
if (!tapOpen) {
return Uni.createFrom().voidItem();
}
return Uni.createFrom().completionStage(orderEmitter.send(order));
}
}This gives us a reliable source of individual messages and a clear completion event.
REST Endpoint for Triggering Orders
Users interact with the pipeline through this endpoint:
package com.acme;
import com.acme.model.Order;
import jakarta.inject.Inject;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
@Path(”/orders”)
public class OrderResource {
@Inject
OrderGateway gateway;
@POST
public void newOrder(Order order) {
gateway.submitOrder(order)
.subscribe().with(unused -> {
}, Throwable::printStackTrace);
}
@POST
@Path(”/open”)
public void open() {
gateway.openTap();
}
@POST
@Path(”/close”)
public void close() {
gateway.closeTap();
}
}Now you have:
POST /orders/open
POST /orders (with JSON body)
POST /orders/closeKitchen Service: Transforming Incoming Orders
This stage receives orders, processes them, and produces enriched KitchenTicket events.
package com.acme;
import org.eclipse.microprofile.reactive.messaging.Incoming;
import org.eclipse.microprofile.reactive.messaging.Outgoing;
import com.acme.model.KitchenTicket;
import com.acme.model.Order;
import io.quarkus.logging.Log;
import jakarta.enterprise.context.ApplicationScoped;
@ApplicationScoped
public class KitchenService {
@Incoming(”orders”)
@Outgoing(”kitchen”)
public KitchenTicket prepare(Order order) {
Log.infof(”Kitchen preparing: %s”, order.item());
return new KitchenTicket(order.id(), order.item(), “PREPARED”, order.price());
}
}This models real-world asynchronous workflows:
orders enter the system → kitchen reacts independently.
Aggregator: Sum All Orders When the Tap Closes
This is where completion signals shine.
The aggregator listens to kitchen events and emits one final total when the orders stream completes.
package com.acme;
import org.eclipse.microprofile.reactive.messaging.Incoming;
import org.eclipse.microprofile.reactive.messaging.Outgoing;
import com.acme.model.KitchenTicket;
import io.smallrye.mutiny.Multi;
import jakarta.enterprise.context.ApplicationScoped;
@ApplicationScoped
public class TotalAggregator {
@Incoming(”kitchen”)
@Outgoing(”totals”)
public Multi<Double> process(Multi<KitchenTicket> tickets) {
return tickets
.collect().asList()
.toMulti()
.map(list -> list.stream().mapToDouble(KitchenTicket::price).sum());
}
}This teaches a core lesson: completion is a first-class event in reactive systems.
Final Notification
This is the final consumer of the pipeline.
package com.acme;
import jakarta.enterprise.context.ApplicationScoped;
import org.eclipse.microprofile.reactive.messaging.Incoming;
import io.quarkus.logging.Log;
@ApplicationScoped
public class NotificationSink {
@Incoming(”totals”)
public void notifyTotal(Double total) {
Log.infof(”Tap closed. Total value of all prepared orders: %s”, total);
}
}One message. One moment. A clean closure to the stream.
Run the Application
Start Quarkus:
quarkus devThen simulate the flow:
Open the tap
curl -X POST http://localhost:8080/orders/openSubmit orders
# Add another order
curl -X POST http://localhost:8080/orders \
-H "Content-Type: application/json" \
-d '{"id":"order-456","item":"Caesar Salad","price":8.50}'# Add another order
curl -X POST http://localhost:8080/orders \
-H "Content-Type: application/json" \
-d '{"id":"order-789","item":"Tiramisu","price":5.00}'Console prints:
Kitchen preparing: Caesar Salad
Kitchen preparing: TiramisuClose the tap
curl -X POST http://localhost:8080/orders/closeConsole prints:
Tap closed. Total value of all prepared orders: 17.0This is the moment your pipeline completes and emits the final aggregated result.
What You Learned About Reactive Messaging in Quarkus
Reactive Messaging helps Java developers move beyond traditional request/response patterns. Instead of services waiting on each other, work is handled asynchronously through channels that connect producers and consumers. In Quarkus, this model is simple: annotated methods define how messages flow, transform, and complete.
A reactive pipeline supports multiple stages without tight coupling. Each stage processes events independently, which improves scalability, resilience, and throughput. Completion events are first-class citizens, making it easy to build workflows that aggregate results or trigger final actions when a stream ends.
This tutorial showed how to:
Use channels to build asynchronous, event-driven workflows.
Produce and consume messages with
@Outgoingand@Incoming.React to stream completion to perform final aggregation.
Decouple services so they scale and fail independently.
Reactive Messaging gives you a clean, Java-first way to build real-time systems without managing low-level messaging APIs.
Production Notes
The
smallrye-in-memoryconnector is ideal for tutorials and tests — not production.For real deployments, use Kafka or AMQP connectors.
Multi-stage pipelines scale horizontally with minimal coordination.
Message ordering and backpressure become important at higher loads.
Next Steps
You now have the foundation to explore even more:
branching pipelines
filtering and transformation
error channels
windowing and batching
Kafka-backed streaming
Check out the excellent Quarkus documentation!
You’ve already built a multi-stage reactive workflow.
Everything from here builds on the same mental model.




