<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>Coder Legion - Recent posts tagged quarkus</title>
<link>https://coderlegion.com/tag/quarkus</link>
<description>Powered by Question2Answer</description>
<item>
<title>quarkus-chat-ui (2): The Actor Design Behind LLM-to-LLM Conversation</title>
<link>https://coderlegion.com/14006/quarkus-chat-ui-2-the-actor-design-behind-llm-to-llm-conversation</link>
<description>&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Originally published on &lt;a rel=&quot;nofollow&quot; href=&quot;https://scivicslab.com/blog/2026-04-05-pojo-actor-llm-conversation&quot;&gt;ScivicsLab Blog&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;In a &lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/devteam2512/pojo-actor-v10-a-lightweight-actor-model-library-for-java-2ccd&quot;&gt;previous post&lt;/a&gt; I introduced POJO-actor — a lightweight actor-model library for Java that needs no framework, no annotation processor, and no external runtime. Just plain Java 21.&lt;/p&gt;
&lt;p&gt;This post is about how I actually use it in &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/scivicslab/quarkus-chat-ui&quot;&gt;quarkus-chat-ui&lt;/a&gt;, a Quarkus-based LLM chat UI that connects to Claude Code CLI, vLLM, and other backends. The application manages stateful LLM sessions, streams responses via SSE, handles concurrent MCP requests, and supports a &lt;code&gt;/btw&lt;/code&gt; command for side questions — all without a single &lt;code&gt;synchronized&lt;/code&gt; block.&lt;/p&gt;
&lt;p&gt;The three actors that make this work are &lt;code&gt;ChatActor&lt;/code&gt;, &lt;code&gt;BtwActor&lt;/code&gt;, and &lt;code&gt;QueueActor&lt;/code&gt;. This post focuses on the first two and the design principle behind them.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;quick-pojo-actor-recap&quot;&gt;Quick POJO-actor recap&lt;/h3&gt;
&lt;p&gt;A POJO-actor actor wraps any plain Java object. Messages are lambdas dispatched through a &lt;code&gt;LinkedBlockingQueue&lt;/code&gt;. The actor runs its message loop on a single virtual thread, processing one message at a time.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;ActorSystem system = new ActorSystem(&quot;chat-ui&quot;);
ActorRef&amp;lt;Counter&amp;gt; counter = system.actorOf(&quot;counter&quot;, new Counter());

// tell() — enqueue, return immediately
counter.tell(c -&amp;gt; c.increment());

// ask() — enqueue, return CompletableFuture with result
int value = counter.ask(c -&amp;gt; c.getValue()).join(); // = 1

// tellNow() — bypass queue, execute on a NEW virtual thread immediately
counter.tellNow(c -&amp;gt; c.emergencyStop());
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The key properties:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;tell()&lt;/code&gt; and &lt;code&gt;ask()&lt;/code&gt;&lt;/strong&gt;: FIFO, safe by default — only one message runs at a time on the actor's virtual thread.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;tellNow()&lt;/code&gt; and &lt;code&gt;askNow()&lt;/code&gt;&lt;/strong&gt;: bypass the queue, run concurrently on a fresh virtual thread. The caller takes responsibility for thread safety.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is the whole API. The class itself is a plain Java object — no annotations, no base class to extend, no interface to implement. Any POJO becomes an actor simply by passing it to &lt;code&gt;ActorSystem.actorOf()&lt;/code&gt;. The object does not know it is running inside an actor.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-problem-blocking-i-o-kills-the-message-loop&quot;&gt;The problem: blocking I/O kills the message loop&lt;/h3&gt;
&lt;p&gt;An LLM API call can take 30–60 seconds. If &lt;code&gt;ChatActor&lt;/code&gt; calls the LLM directly inside a message handler, the actor's message loop is blocked for the entire duration:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;tell(startPrompt) → message.run() → provider.sendPrompt() ← blocks for 30s
                                                               |
tell(cancel)  ← enqueued, but not processed until sendPrompt() returns
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The cancel button press arrives as a &lt;code&gt;tell(cancel)&lt;/code&gt;, but the actor won't see it until the blocking call returns. That's useless.&lt;/p&gt;
&lt;p&gt;The fix: delegate the blocking I/O to a separate virtual thread and return immediately.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;chatactor-the-state-guardian&quot;&gt;ChatActor: the state guardian&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;ChatActor&lt;/code&gt; holds all session state — API key, model, history, &lt;code&gt;busy&lt;/code&gt; flag. It protects that state by being the single thread that reads and writes it.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class ChatActor {

    private final LlmProvider provider;
    private volatile Thread activeThread;  // written by actor thread, read by cancel()

    private boolean busy;
    private String apiKey;
    private String sessionId;
    // ... other session state

    public void startPrompt(String text, String model, Consumer&amp;lt;ChatEvent&amp;gt; emitter,
                            ActorRef&amp;lt;ChatActor&amp;gt; self, Runnable done) {
        busy = true;
        emitter.accept(ChatEvent.status(sessionId, true));

        // Delegate the blocking I/O to a virtual thread and return immediately.
        // The actor's message loop is now free to process the next message.
        activeThread = Thread.startVirtualThread(() -&amp;gt; {
            try {
                ProviderContext ctx = new ProviderContext(apiKey, history, false, () -&amp;gt; {});
                provider.sendPrompt(text, model, emitter, ctx);
            } catch (Exception e) {
                emitter.accept(ChatEvent.error(e.getMessage()));
            } finally {
                // Report completion back through the actor's queue (thread-safe)
                self.tell(a -&amp;gt; a.onPromptComplete(emitter));
            }
        });
        // ← returns here immediately. The actor is ready for the next message.
    }

    public void cancel() {
        provider.cancel();
        Thread t = activeThread;       // read volatile field
        if (t != null) t.interrupt(); // interrupt the I/O virtual thread
    }

    private void onPromptComplete(Consumer&amp;lt;ChatEvent&amp;gt; emitter) {
        busy = false;
        activeThread = null;
        emitter.accept(ChatEvent.status(sessionId, false));
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The message flow after &lt;code&gt;startPrompt()&lt;/code&gt; is called:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;tell(startPrompt) → starts I/O thread → returns immediately
                    actor loop is now idle, waiting for next message

tell(cancel)      → no queue wait → cancel() runs → I/O thread interrupted
                    I/O thread catches exception → finally → self.tell(onPromptComplete)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is the core pattern. The actor is the &lt;strong&gt;state guardian&lt;/strong&gt;; the virtual thread is the &lt;strong&gt;I/O worker&lt;/strong&gt;. The actor never blocks. Every cancel, every MCP request, every queue tick finds the actor ready to respond.&lt;/p&gt;
&lt;h4 id=&quot;why-volatile-thread-activethread-&quot;&gt;Why &lt;code&gt;volatile Thread activeThread&lt;/code&gt;?&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;activeThread&lt;/code&gt; is written by the actor's virtual thread (in &lt;code&gt;startPrompt&lt;/code&gt;) and read by &lt;code&gt;cancel()&lt;/code&gt;. Normally both are on the same actor thread, so &lt;code&gt;volatile&lt;/code&gt; is redundant — but &lt;code&gt;cancel()&lt;/code&gt; is also called via &lt;code&gt;tellNow()&lt;/code&gt;, which runs on a &lt;em&gt;different&lt;/em&gt; virtual thread. &lt;code&gt;volatile&lt;/code&gt; ensures the write to &lt;code&gt;activeThread&lt;/code&gt; is visible across that thread boundary. It's a defensive declaration that costs nothing and prevents a subtle bug.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;why-tellnow-for-cancel-&quot;&gt;Why &lt;code&gt;tellNow()&lt;/code&gt; for cancel?&lt;/h3&gt;
&lt;p&gt;The REST endpoint that handles the cancel button:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@POST
@Path(&quot;/cancel&quot;)
public ChatEvent cancel() {
    actorSystem.getChatActor().tellNow(ChatActor::cancel);
    return ChatEvent.info(&quot;Cancelled&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Why &lt;code&gt;tellNow()&lt;/code&gt; and not &lt;code&gt;tell()&lt;/code&gt;?&lt;/p&gt;
&lt;p&gt;With &lt;code&gt;tell()&lt;/code&gt;, &lt;code&gt;cancel()&lt;/code&gt; would be enqueued behind whatever is currently in the queue. But &lt;code&gt;startPrompt()&lt;/code&gt; delegates its I/O immediately and returns — so in practice, the actor's queue is already empty and &lt;code&gt;tell(cancel)&lt;/code&gt; would be processed right away. The behavior is correct either way.&lt;/p&gt;
&lt;p&gt;The reason to use &lt;code&gt;tellNow()&lt;/code&gt; is &lt;strong&gt;semantic clarity&lt;/strong&gt;: &quot;cancel NOW&quot; — not &quot;cancel when it's your turn&quot;. It also makes the intent obvious to readers of the code. &lt;code&gt;cancel()&lt;/code&gt; touches only &lt;code&gt;volatile activeThread&lt;/code&gt; and &lt;code&gt;provider.cancel()&lt;/code&gt;, both of which are designed for concurrent access, so bypassing the queue is safe here.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;btwactor-a-second-independent-actor&quot;&gt;BtwActor: a second independent actor&lt;/h3&gt;
&lt;p&gt;Claude Code CLI has a &lt;code&gt;/btw&lt;/code&gt; command: while the LLM is processing a long task, you can type &lt;code&gt;/btw what does this mean?&lt;/code&gt; and get an immediate answer from a parallel LLM call, without interrupting the main task.&lt;/p&gt;
&lt;p&gt;quarkus-chat-ui implements the same UX. The Web UI detects the &lt;code&gt;/btw&lt;/code&gt; prefix and sends the question to &lt;code&gt;POST /api/btw&lt;/code&gt;. The response appears in a floating overlay in the bottom-right corner; the main chat continues uninterrupted.&lt;/p&gt;
&lt;p&gt;The key insight: this is just a second independent actor that follows the same pattern as &lt;code&gt;ChatActor&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class BtwActor {

    private final LlmProvider provider;
    private volatile Thread activeThread;

    public void startBtw(String question, String model, String apiKey,
                         Consumer&amp;lt;ChatEvent&amp;gt; emitter, ActorRef&amp;lt;BtwActor&amp;gt; self) {
        activeThread = Thread.startVirtualThread(() -&amp;gt; {
            try {
                ProviderContext ctx = new ProviderContext(apiKey, List.of(), false, () -&amp;gt; {});
                provider.sendPrompt(question, model, event -&amp;gt; {
                    // Map provider events to btw-specific SSE event types
                    if (&quot;delta&quot;.equals(event.type()))  emitter.accept(ChatEvent.btwDelta(event.content()));
                    if (&quot;result&quot;.equals(event.type())) emitter.accept(ChatEvent.btwResult());
                }, ctx);
            } catch (Exception e) {
                emitter.accept(ChatEvent.error(&quot;BTW error: &quot; + e.getMessage()));
            } finally {
                self.tell(a -&amp;gt; a.activeThread = null);
            }
        });
    }

    public void cancel() {
        Thread t = activeThread;
        if (t != null) { provider.cancel(); t.interrupt(); }
    }

    public boolean isBusy() { return activeThread != null; }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;BtwActor&lt;/code&gt; is registered alongside &lt;code&gt;ChatActor&lt;/code&gt; in &lt;code&gt;LlmConsoleActorSystem&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;chatActorRef = actorSystem.actorOf(&quot;chat&quot;, new ChatActor(provider, configApiKey));
btwActorRef  = actorSystem.actorOf(&quot;btw&quot;,  new BtwActor(provider));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The REST endpoint is three lines:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@POST
@Path(&quot;/btw&quot;)
public ChatEvent btw(BtwRequest request) {
    String apiKey = actorSystem.getChatActor().ask(ChatActor::getApiKey).join();
    String model  = request.model != null ? request.model : provider.getCurrentModel();
    var btwRef    = actorSystem.getBtwActor();
    btwRef.tell(a -&amp;gt; a.startBtw(request.question, model, apiKey, this::emitSse, btwRef));
    return ChatEvent.info(&quot;BTW processing&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The frontend receives &lt;code&gt;btw_delta&lt;/code&gt; and &lt;code&gt;btw_result&lt;/code&gt; SSE events (distinct from the main chat's &lt;code&gt;delta&lt;/code&gt;/&lt;code&gt;result&lt;/code&gt;), streams the response into the overlay, and closes it on ESC or the × button.&lt;/p&gt;
&lt;h4 id=&quot;thread-safety-with-a-shared-provider&quot;&gt;Thread safety with a shared provider&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;ChatActor&lt;/code&gt; and &lt;code&gt;BtwActor&lt;/code&gt; share the same &lt;code&gt;LlmProvider&lt;/code&gt; instance. For HTTP-based providers (Anthropic API, vLLM), each &lt;code&gt;sendPrompt()&lt;/code&gt; call is an independent HTTP request — parallel calls are safe by construction.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;BtwActor&lt;/code&gt; intentionally does not call &lt;code&gt;provider.setModel()&lt;/code&gt; before &lt;code&gt;sendPrompt()&lt;/code&gt;. Calling &lt;code&gt;setModel()&lt;/code&gt; would mutate shared provider state, potentially corrupting &lt;code&gt;ChatActor&lt;/code&gt;'s current model mid-stream. Instead, the model is passed directly as a parameter to &lt;code&gt;sendPrompt()&lt;/code&gt;. The actors are isolated at the state level even though they share the provider.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-actor-system-structure&quot;&gt;The actor system structure&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;ActorSystem &quot;chat-ui&quot;
  ├── ChatActor    — session state, model selection, history
  ├── BtwActor     — /btw side questions (independent)
  ├── QueueActor   — message queuing for MCP/concurrent requests
  └── WatchdogActor — stall detection (CLI providers only)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;All four follow the same pattern: the actor is the state guardian, virtual threads handle I/O. &lt;code&gt;tell()&lt;/code&gt; for normal messages, &lt;code&gt;tellNow()&lt;/code&gt; for urgent bypasses that are provably safe.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-i-like-about-this-approach&quot;&gt;What I like about this approach&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;No framework tax.&lt;/strong&gt; Each actor is a POJO. The actor model adds one concept (&lt;code&gt;tell&lt;/code&gt; vs &lt;code&gt;tellNow&lt;/code&gt;) on top of plain Java. Reading &lt;code&gt;ChatActor&lt;/code&gt; requires no knowledge of Akka, Quarkus, or any actor framework — just Java.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The virtual thread + actor split is explicit.&lt;/strong&gt; The code visually shows the boundary: &lt;code&gt;Thread.startVirtualThread(...)&lt;/code&gt; inside &lt;code&gt;startPrompt()&lt;/code&gt; is the handoff from &quot;actor's domain&quot; to &quot;I/O worker's domain&quot;. When something goes wrong, it's immediately clear which side owns the problem.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Adding actors is cheap.&lt;/strong&gt; &lt;code&gt;BtwActor&lt;/code&gt; is 30 lines. Adding &lt;code&gt;/btw cancel&lt;/code&gt; in the future is one &lt;code&gt;tellNow()&lt;/code&gt; call. The pattern scales to new actors (WatchdogActor, QueueActor) without changing the existing ones.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;State bugs are localized.&lt;/strong&gt; &lt;code&gt;ChatActor&lt;/code&gt; is the only place that can modify session state. If the session ID is wrong, the bug is in &lt;code&gt;ChatActor&lt;/code&gt;. There is no &quot;it could be any thread&quot; scenario.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;links&quot;&gt;Links&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;./040_McpGatewayMultiAgent_260405_oo01.md&quot;&gt;quarkus-chat-ui (3): Scaling Multi-Agent Communication with MCP Gateway&lt;/a&gt; — the next post in this series&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/devteam2512/quarkus-chat-ui-a-web-front-end-for-llms-and-a-real-world-case-for-pojo-actor-5e92&quot;&gt;quarkus-chat-ui: A Web Front-End for LLMs, and a Real-World Case for POJO-actor&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;quarkus-chat-UI Official Manual : &lt;a rel=&quot;nofollow&quot; href=&quot;https://scivicslab.com/docs/ai-tools/quarkus-chat-ui&quot;&gt;https://scivicslab.com/docs/ai-tools/quarkus-chat-ui&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/scivicslab/quarkus-chat-ui&quot;&gt;quarkus-chat-ui on GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/scivicslab/quarkus-mcp-gateway&quot;&gt;quarkus-mcp-gateway on GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;POJO-actor Official Manual: &lt;a rel=&quot;nofollow&quot; href=&quot;https://scivicslab.com/docs/pojo-actor/introduction&quot;&gt;https://scivicslab.com/docs/pojo-actor/introduction&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;GitHub (POJO-actor): &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/scivicslab/POJO-actor&quot;&gt;https://github.com/scivicslab/POJO-actor&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Maven Repository: &lt;a rel=&quot;nofollow&quot; href=&quot;https://mvnrepository.com/search?q=com.scivicslab&quot;&gt;https://mvnrepository.com/search?q=com.scivicslab&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;POJO-actor v1.0 Introduction (blog): A Lightweight Actor Model Library for Java : &lt;a rel=&quot;nofollow&quot; href=&quot;https://scivicslab.com/blog/2025-12-22-pojo-actor-v1-introduction&quot;&gt;https://scivicslab.com/blog/2025-12-22-pojo-actor-v1-introduction&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/14006/quarkus-chat-ui-2-the-actor-design-behind-llm-to-llm-conversation</guid>
<pubDate>Sun, 05 Apr 2026 10:01:50 +0000</pubDate>
</item>
<item>
<title>quarkus-chat-ui: A Web Front-End for LLMs, and a Real-World Case for POJO-actor</title>
<link>https://coderlegion.com/14005/quarkus-chat-ui-a-web-front-end-for-llms-and-a-real-world-case-for-pojo-actor</link>
<description>&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; This article was originally published on &lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://scivicslab.com/blog/2026-04-05-quarkus-chat-ui-intro&quot;&gt;SciVicsLab&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;quarkus-chat-ui-a-web-front-end-for-llms-and-a-real-world-case-for-pojo-actor&quot;&gt;quarkus-chat-ui: A Web Front-End for LLMs, and a Real-World Case for POJO-actor&lt;/h2&gt;
&lt;p&gt;quarkus-chat-ui is a web UI for LLMs where multiple instances can talk to each other — built as a real-world use case for &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/scivicslab/pojo-actor&quot;&gt;POJO-actor&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Each quarkus-chat-ui instance exposes an HTTP MCP server at &lt;code&gt;/mcp&lt;/code&gt;, so Instance A can call tools on Instance B, and Instance B can reply by calling tools back on A. The LLM backend — Claude Code CLI, Codex, or a local model via claw-code-local — acts as an MCP client that can reach these endpoints. The question was how to wire that up over HTTP, and how to handle the fact that LLM responses take tens of seconds and arrive as a stream.&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/scivicslab/quarkus-chat-ui&quot;&gt;quarkus-chat-ui&lt;/a&gt; is the bridge that makes this work. Each instance wraps one LLM backend and exposes it as an HTTP MCP server at &lt;code&gt;/mcp&lt;/code&gt;. For multi-agent communication, use a backend with MCP client capability: Claude Code CLI, Codex, or claw-code-local (which brings MCP support to Ollama, vLLM, and other local models). The &lt;code&gt;openai-compat&lt;/code&gt; provider works for single-agent use but cannot call other MCP servers. Agents call each other by name. Humans can watch both sides of the conversation in their browsers.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=2866985040903834289&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Once the async communication layer was in place, a capable web UI and a prompt queue came along naturally. The browser gives you a stable place to type — your input won't vanish when the AI responds, and paste and multi-line just work. If you need an LLM front-end and happen to be a Java developer, those turn out to be useful in their own right.&lt;/p&gt;
&lt;p&gt;This post is about quarkus-chat-ui as a tool you can use. Companion posts cover:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The internal design — how &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/scivicslab/pojo-actor&quot;&gt;POJO-actor&lt;/a&gt; keeps the concurrency clean → &lt;a rel=&quot;nofollow&quot; href=&quot;./030_PojoActorInPractice_260405_oo01.md&quot;&gt;quarkus-chat-ui (2): The Actor Design Behind LLM-to-LLM Communication&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Scaling beyond two agents — why quarkus-mcp-gateway becomes necessary → &lt;a rel=&quot;nofollow&quot; href=&quot;./040_McpGatewayMultiAgent_260405_oo01.md&quot;&gt;quarkus-chat-ui (3): Scaling Multi-Agent Communication with MCP Gateway&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-it-does&quot;&gt;What it does&lt;/h3&gt;
&lt;h4 id=&quot;1-llm-instances-talking-to-each-other-via-mcp&quot;&gt;1. LLM instances talking to each other via MCP&lt;/h4&gt;
&lt;p&gt;Each quarkus-chat-ui instance exposes an HTTP MCP server at &lt;code&gt;/mcp&lt;/code&gt;. The tools are &lt;code&gt;submitPrompt&lt;/code&gt;, &lt;code&gt;getPromptStatus&lt;/code&gt;, &lt;code&gt;getPromptResult&lt;/code&gt;, &lt;code&gt;cancelRequest&lt;/code&gt;, and a few others.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;submitPrompt&lt;/code&gt; accepts a &lt;code&gt;_caller&lt;/code&gt; parameter. When Instance B receives a prompt with &lt;code&gt;_caller&lt;/code&gt; pointing back to A, it enriches the prompt with position awareness and reply instructions before forwarding it to its LLM:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;[Context]
You are running on: http://localhost:28020
Received via MCP from: localhost:28010

[Message]
What should we work on today?

[How to Reply]
Use callMcpServer tool:
- serverUrl: http://localhost:28010
- toolName: submitPrompt
- arguments: {&quot;prompt&quot;: &quot;your reply&quot;, &quot;_caller&quot;: &quot;http://localhost:28020&quot;}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The LLM reads this, formulates a reply, and calls &lt;code&gt;submitPrompt&lt;/code&gt; on Instance A. The conversation continues autonomously.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;Browser A (port 28010)             Browser B (port 28020)
──────────────────────             ──────────────────────
[MCP from localhost:28020]         [MCP from localhost:28010]
What should we work on today? ←→   Let's start with the API layer.
                                   [MCP from localhost:28010]
[MCP from localhost:28020]         ...
Good idea. Let's define...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can also put a &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/scivicslab/quarkus-mcp-gateway&quot;&gt;quarkus-mcp-gateway&lt;/a&gt; in front of many instances, routing by name instead of port. Service discovery scans a port range and registers all running agents automatically.&lt;/p&gt;
&lt;h4 id=&quot;2-written-in-quarkus-streaming-made-simple&quot;&gt;2. Written in Quarkus — streaming made simple&lt;/h4&gt;
&lt;p&gt;LLM tooling tends to be Python. If you want to customise how prompts are enriched, add a new backend, or change the queue policy, quarkus-chat-ui is a straightforward Quarkus Maven project.&lt;/p&gt;
&lt;p&gt;Adding a new LLM backend means implementing one interface:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public interface LlmProvider {
    String id();
    void sendPrompt(String prompt, String model,
                    Consumer&amp;lt;ChatEvent&amp;gt; emitter, ProviderContext ctx);
    void cancel();
    // ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;SSE streaming, queue management, MCP server, and conversation history are already there.&lt;/p&gt;
&lt;p&gt;Quarkus made the streaming part almost trivial. Exposing an SSE endpoint is a matter of returning &lt;code&gt;Multi&amp;lt;T&amp;gt;&lt;/code&gt; from a JAX-RS method:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@GET @Path(&quot;/events/{sessionId}&quot;)
@Produces(MediaType.SERVER_SENT_EVENTS)
@RestStreamElementType(MediaType.APPLICATION_JSON)
public Multi&amp;lt;ChatEvent&amp;gt; events(@PathParam(&quot;sessionId&quot;) String sessionId) {
    return chatService.getEventStream(sessionId);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That is all. The framework handles chunked encoding, keep-alive, and client reconnection. Backpressure flows naturally through Mutiny's reactive streams. There is no manual buffer management, no explicit flush calls, no thread-pool tuning. You return a &lt;code&gt;Multi&lt;/code&gt;, Quarkus streams it.&lt;/p&gt;
&lt;h4 id=&quot;3-a-prompt-queue-that-does-the-obvious-thing&quot;&gt;3. A prompt queue that does the obvious thing&lt;/h4&gt;
&lt;p&gt;When the LLM is busy and you want to queue your next question, you can. When the current response finishes, the queued prompt runs automatically.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;[LLM is processing &quot;Explain this code&quot;]

You type: &quot;Now write the tests&quot;    →  added to queue
You type: &quot;And the documentation&quot;  →  added to queue

[Response arrives]
→ &quot;Now write the tests&quot; runs automatically
[Response arrives]
→ &quot;And the documentation&quot; runs automatically
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The queue is visible in the UI, persistent across page reloads, and editable — reorder or delete items before they run.&lt;/p&gt;
&lt;p&gt;Cancel works correctly in the multi-agent case too. Pressing Cancel stops the current generation and removes MCP-sourced messages from the backend queue. Messages you typed yourself stay in the queue and run after the cancel.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;why-the-concurrency-is-manageable&quot;&gt;Why the concurrency is manageable&lt;/h3&gt;
&lt;p&gt;Multi-agent HTTP conversation sounds like a concurrency nightmare: SSE streams arriving from multiple agents, a queue draining as responses land, cancel signals that need to reach the right places. In practice it is not, because the design is explicit about who owns what state.&lt;/p&gt;
&lt;p&gt;Each concern — chat session, side questions, queue management, stall detection — runs in its own actor backed by &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/scivicslab/pojo-actor&quot;&gt;POJO-actor&lt;/a&gt;. Blocking I/O runs on virtual threads that report back to the actor when done. The actors communicate through &lt;code&gt;tell()&lt;/code&gt; and &lt;code&gt;ask()&lt;/code&gt; calls. There are no &lt;code&gt;synchronized&lt;/code&gt; blocks in the application code.&lt;/p&gt;
&lt;p&gt;The companion post goes into detail: &lt;a rel=&quot;nofollow&quot; href=&quot;./030_PojoActorInPractice_260405_oo01.md&quot;&gt;quarkus-chat-ui (2): The Actor Design Behind LLM-to-LLM Communication&lt;/a&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;quick-start&quot;&gt;Quick start&lt;/h3&gt;
&lt;p&gt;Three providers are supported:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;&lt;code&gt;chat-ui.provider&lt;/code&gt; &lt;/th&gt;&lt;th&gt;What it wraps &lt;/th&gt;&lt;th&gt;Requires&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;claude&lt;/code&gt; &lt;/td&gt;&lt;td&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.anthropic.com/en/docs/claude-code&quot;&gt;Claude Code CLI&lt;/a&gt; &lt;/td&gt;&lt;td&gt;&lt;code&gt;ANTHROPIC_API_KEY&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;codex&lt;/code&gt; &lt;/td&gt;&lt;td&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/openai/codex&quot;&gt;OpenAI Codex CLI&lt;/a&gt; &lt;/td&gt;&lt;td&gt;&lt;code&gt;OPENAI_API_KEY&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;openai-compat&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Any OpenAI-compatible HTTP server (vLLM, Ollama, …) &lt;/td&gt;&lt;td&gt;running local server&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4 id=&quot;preparing-a-local-llm-openai-compat-&quot;&gt;Preparing a local LLM (openai-compat)&lt;/h4&gt;
&lt;p&gt;If you want to run a local model instead of a cloud API, &lt;a rel=&quot;nofollow&quot; href=&quot;https://ollama.com/&quot;&gt;Ollama&lt;/a&gt; is the easiest way to get started:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Install Ollama, then pull a model
ollama pull qwen2.5-coder:7b

# Ollama listens on http://localhost:11434 by default
# Use -Dchat-ui.servers=http://localhost:11434/v1 when starting quarkus-chat-ui
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For GPU-accelerated inference, &lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.vllm.ai/&quot;&gt;vLLM&lt;/a&gt; serves any HuggingFace-compatible model on the same OpenAI-compatible API:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;vllm serve Qwen/Qwen2.5-Coder-7B-Instruct --port 8000
# Use -Dchat-ui.servers=http://localhost:8000
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;option-1-native-executable-no-jdk-required-&quot;&gt;Option 1: Native executable (no JDK required)&lt;/h4&gt;
&lt;p&gt;Java traditionally requires a JVM to run. You install a JDK, compile your code to bytecode, and the JVM interprets or JIT-compiles it at runtime. This is why &quot;installing Java&quot; has always been a prerequisite for running Java applications.&lt;/p&gt;
&lt;p&gt;GraalVM native image changes this. It compiles Java bytecode ahead-of-time into a native executable for your OS and CPU architecture. The result is a standalone binary — just like a C or Go program. No JVM, no JDK, no &lt;code&gt;JAVA_HOME&lt;/code&gt;. Download, run, done.&lt;/p&gt;
&lt;p&gt;This is where Quarkus shines again. Traditional Java frameworks rely heavily on runtime reflection, making native image compilation painful — you end up maintaining long lists of reflection configuration by hand. Quarkus was designed from the start with native compilation in mind. It moves reflection and configuration processing to build time, and its extensions generate the necessary GraalVM hints automatically. You just run &lt;code&gt;mvn package -Dnative&lt;/code&gt; and it works.&lt;/p&gt;
&lt;p&gt;Pre-built native executables are available on the &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/scivicslab/quarkus-chat-ui/releases&quot;&gt;Releases&lt;/a&gt; page.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Platform &lt;/th&gt;&lt;th&gt;Binary&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Linux x86_64 &lt;/td&gt;&lt;td&gt;&lt;code&gt;quarkus-chat-ui-linux-amd64&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Linux ARM64 &lt;/td&gt;&lt;td&gt;&lt;code&gt;quarkus-chat-ui-linux-arm64&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;macOS Intel &lt;/td&gt;&lt;td&gt;&lt;code&gt;quarkus-chat-ui-macos-amd64&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;macOS Apple Silicon (M1/M2/M3) &lt;/td&gt;&lt;td&gt;&lt;code&gt;quarkus-chat-ui-macos-arm64&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Windows x64 &lt;/td&gt;&lt;td&gt;&lt;code&gt;quarkus-chat-ui-windows-amd64.exe&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Linux x86_64
./quarkus-chat-ui-linux-amd64 -Dchat-ui.provider=claude -Dquarkus.http.port=28010

# Linux ARM64
./quarkus-chat-ui-linux-arm64 -Dchat-ui.provider=claude -Dquarkus.http.port=28010

# macOS Intel
./quarkus-chat-ui-macos-amd64 -Dchat-ui.provider=claude -Dquarkus.http.port=28010

# macOS Apple Silicon (M1/M2/M3)
./quarkus-chat-ui-macos-arm64 -Dchat-ui.provider=claude -Dquarkus.http.port=28010
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-powershell&quot;&gt;# Windows PowerShell
.\quarkus-chat-ui-windows-amd64.exe -Dchat-ui.provider=claude -Dquarkus.http.port=28010
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;option-2-build-from-source&quot;&gt;Option 2: Build from source&lt;/h4&gt;
&lt;p&gt;Prerequisites: JDK 21+ and Maven 3.x&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git clone https://github.com/scivicslab/quarkus-chat-ui
cd quarkus-chat-ui
mvn install -DskipTests
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Run with Claude Code CLI:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;java -Dchat-ui.provider=claude \
     -Dquarkus.http.port=28010 \
     -jar app/target/quarkus-app/quarkus-run.jar
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Run with vLLM or Ollama (OpenAI-compatible API):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;java -Dchat-ui.provider=openai-compat \
     -Dchat-ui.openai-compat.base-url=http://localhost:11434/v1 \
     -Dquarkus.http.port=28010 \
     -jar app/target/quarkus-app/quarkus-run.jar
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Open &lt;code&gt;http://localhost:28010&lt;/code&gt; in a browser.&lt;/p&gt;
&lt;p&gt;For all providers (&lt;code&gt;claude&lt;/code&gt;, &lt;code&gt;codex&lt;/code&gt;, &lt;code&gt;openai-compat&lt;/code&gt;) and configuration options, see the &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/scivicslab/quarkus-chat-ui#configuration&quot;&gt;README&lt;/a&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;setting-up-two-agents-to-talk-claude-code-cli-&quot;&gt;Setting up two agents to talk (Claude Code CLI)&lt;/h3&gt;
&lt;p&gt;Two quarkus-chat-ui instances talking to each other via MCP.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Overview:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;You will set up two instances (Alice and Bob) that can send messages to each other via MCP:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Alice runs on port 28010&lt;/li&gt;
&lt;li&gt;Bob runs on port 28020&lt;/li&gt;
&lt;li&gt;Each instance has its own browser window&lt;/li&gt;
&lt;li&gt;Claude Code CLI acts as the MCP client for both instances&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Prerequisites:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;quarkus-chat-ui repository cloned and built (see &quot;Option 2: Build from source&quot; above)&lt;/li&gt;
&lt;li&gt;Working directory: &lt;code&gt;quarkus-chat-ui/&lt;/code&gt; (repository root)&lt;/li&gt;
&lt;li&gt;3 terminal windows (or tabs)&lt;/li&gt;
&lt;li&gt;2 browser windows (or tabs)&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;1-install-claude-code-cli&quot;&gt;1. Install Claude Code CLI&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;Where:&lt;/strong&gt; Any directory&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm install -g @anthropic-ai/claude-code
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Verify:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;claude --version
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;2-set-api-key&quot;&gt;2. Set API Key&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;Where:&lt;/strong&gt; Any terminal (environment variable applies to all subsequent commands in that terminal)&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;export ANTHROPIC_API_KEY=sk-ant-api03-...
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;3-start-alice-port-28010-&quot;&gt;3. Start Alice (port 28010)&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;Terminal 1, working directory:&lt;/strong&gt; &lt;code&gt;quarkus-chat-ui/&lt;/code&gt; (repository root)&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;java -Dchat-ui.provider=claude \
     -Dquarkus.http.port=28010 \
     -jar app/target/quarkus-app/quarkus-run.jar
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Open &lt;a rel=&quot;nofollow&quot; href=&quot;http://localhost:28010&quot;&gt;http://localhost:28010&lt;/a&gt; in your browser&lt;/p&gt;
&lt;h4 id=&quot;4-start-bob-port-28020-&quot;&gt;4. Start Bob (port 28020)&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;Terminal 2, working directory:&lt;/strong&gt; &lt;code&gt;quarkus-chat-ui/&lt;/code&gt; (repository root)&lt;/p&gt;
&lt;p&gt;First, set the API key in this terminal too:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;export ANTHROPIC_API_KEY=sk-ant-api03-...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then start Bob:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;java -Dchat-ui.provider=claude \
     -Dquarkus.http.port=28020 \
     -jar app/target/quarkus-app/quarkus-run.jar
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Open &lt;a rel=&quot;nofollow&quot; href=&quot;http://localhost:28020&quot;&gt;http://localhost:28020&lt;/a&gt; in another browser tab&lt;/p&gt;
&lt;h4 id=&quot;5-register-mcp-endpoints&quot;&gt;5. Register MCP Endpoints&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;Terminal 3, working directory:&lt;/strong&gt; Any directory (MCP registrations are stored in &lt;code&gt;~/.claude/&lt;/code&gt;)&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# So Alice can call Bob
claude mcp add bob --transport http http://localhost:28020/mcp

# So Bob can call Alice
claude mcp add alice --transport http http://localhost:28010/mcp
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Verify the registrations:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;claude mcp list
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Expected output:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;alice: http (http://localhost:28010/mcp)
bob: http (http://localhost:28020/mcp)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;6-restart-both-instances&quot;&gt;6. Restart Both Instances&lt;/h4&gt;
&lt;p&gt;Claude Code CLI reads MCP registrations at startup. Stop and restart both.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Terminal 1&lt;/strong&gt; (Ctrl+C to stop, then restart Alice):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;java -Dchat-ui.provider=claude \
     -Dquarkus.http.port=28010 \
     -jar app/target/quarkus-app/quarkus-run.jar
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Terminal 2&lt;/strong&gt; (Ctrl+C to stop, then restart Bob):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;java -Dchat-ui.provider=claude \
     -Dquarkus.http.port=28020 \
     -jar app/target/quarkus-app/quarkus-run.jar
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;7-test-alice-sends-to-bob&quot;&gt;7. Test: Alice Sends to Bob&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;Where:&lt;/strong&gt; Alice's browser (&lt;a rel=&quot;nofollow&quot; href=&quot;http://localhost:28010&quot;&gt;http://localhost:28010&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;In the prompt input field, type:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;Use mcp__bob__submitPrompt to send &quot;Hello Bob!&quot; to Bob.
Set _caller to http://localhost:28010
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then click Send. Claude will use the MCP tool to send the message to Bob.&lt;/p&gt;
&lt;h4 id=&quot;8-verify-bob-receives&quot;&gt;8. Verify: Bob Receives&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;Where:&lt;/strong&gt; Bob's browser (&lt;a rel=&quot;nofollow&quot; href=&quot;http://localhost:28020&quot;&gt;http://localhost:28020&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;You should see the message appear in Bob's chat area:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;[MCP from localhost:28010] Hello Bob!
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Testing the reply:&lt;/strong&gt; In Bob's browser, type:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;Use mcp__alice__submitPrompt to reply &quot;Hi Alice!&quot; to Alice.
Set _caller to http://localhost:28020
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then check Alice's browser — the reply should appear there.&lt;/p&gt;
&lt;h4 id=&quot;cleanup&quot;&gt;Cleanup&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;Where:&lt;/strong&gt; Any terminal&lt;/p&gt;
&lt;p&gt;When you're done testing, remove the MCP registrations:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;claude mcp remove alice
claude mcp remove bob
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Verify they're removed:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;claude mcp list
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;troubleshooting&quot;&gt;Troubleshooting&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Problem &lt;/th&gt;&lt;th&gt;Solution&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;mcp__bob__submitPrompt&lt;/code&gt; not found &lt;/td&gt;&lt;td&gt;Run &lt;code&gt;claude mcp add bob --transport http http://localhost:28020/mcp&lt;/code&gt; and restart Alice (Terminal 1)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Connection refused &lt;/td&gt;&lt;td&gt;Check that the target instance is running on the expected port (use &lt;code&gt;lsof -i :28010&lt;/code&gt; or &lt;code&gt;lsof -i :28020&lt;/code&gt;)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Bob can't reply to Alice &lt;/td&gt;&lt;td&gt;Run &lt;code&gt;claude mcp add alice --transport http http://localhost:28010/mcp&lt;/code&gt; and restart Bob (Terminal 2)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;ANTHROPIC_API_KEY&lt;/code&gt; not found &lt;/td&gt;&lt;td&gt;Set the environment variable in each terminal: &lt;code&gt;export ANTHROPIC_API_KEY=sk-ant-api03-...&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;app/target/quarkus-app/quarkus-run.jar&lt;/code&gt; not found &lt;/td&gt;&lt;td&gt;Run &lt;code&gt;mvn install -DskipTests&lt;/code&gt; from the &lt;code&gt;quarkus-chat-ui/&lt;/code&gt; repository root&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4 id=&quot;beyond-two-agents&quot;&gt;Beyond two agents&lt;/h4&gt;
&lt;p&gt;For three or more agents, use &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/scivicslab/quarkus-mcp-gateway&quot;&gt;quarkus-mcp-gateway&lt;/a&gt;. See &lt;a rel=&quot;nofollow&quot; href=&quot;./040_McpGatewayMultiAgent_260405_oo01.md&quot;&gt;quarkus-chat-ui (3): Scaling Multi-Agent Communication with MCP Gateway&lt;/a&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;links-1&quot;&gt;Links&lt;/h3&gt;
&lt;p&gt;quarkus-chat-UI Official Manual : &lt;a rel=&quot;nofollow&quot; href=&quot;https://scivicslab.com/docs/ai-tools/quarkus-chat-ui&quot;&gt;https://scivicslab.com/docs/ai-tools/quarkus-chat-ui&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/scivicslab/quarkus-chat-ui&quot;&gt;quarkus-chat-ui on GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/scivicslab/quarkus-mcp-gateway&quot;&gt;quarkus-mcp-gateway on GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;POJO-actor Official Manual: &lt;a rel=&quot;nofollow&quot; href=&quot;https://scivicslab.com/docs/pojo-actor/introduction&quot;&gt;https://scivicslab.com/docs/pojo-actor/introduction&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;GitHub (POJO-actor): &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/scivicslab/POJO-actor&quot;&gt;https://github.com/scivicslab/POJO-actor&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Maven Repository: &lt;a rel=&quot;nofollow&quot; href=&quot;https://mvnrepository.com/search?q=com.scivicslab&quot;&gt;https://mvnrepository.com/search?q=com.scivicslab&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;POJO-actor v1.0 Introduction (blog): A Lightweight Actor Model Library for Java : &lt;a rel=&quot;nofollow&quot; href=&quot;https://scivicslab.com/blog/2025-12-22-pojo-actor-v1-introduction&quot;&gt;https://scivicslab.com/blog/2025-12-22-pojo-actor-v1-introduction&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/14005/quarkus-chat-ui-a-web-front-end-for-llms-and-a-real-world-case-for-pojo-actor</guid>
<pubDate>Sun, 05 Apr 2026 08:04:37 +0000</pubDate>
</item>
<item>
<title>Quarkus: Revolutionizing Java Development for the Cloud-Native Era</title>
<link>https://coderlegion.com/5654/quarkus-revolutionizing-java-development-for-the-cloud-native-era</link>
<description>&lt;p&gt;In the rapidly evolving landscape of cloud-native development, &lt;strong&gt;Java&lt;/strong&gt; has faced criticism for being slow to start and memory-hungry compared to newer technologies. Enter &lt;strong&gt;Quarkus&lt;/strong&gt;, a framework that promises to make Java &lt;strong&gt;“supersonic and subatomic”&lt;/strong&gt; while maintaining the rich ecosystem and developer experience Java developers love.&lt;/p&gt;
&lt;p&gt;If you’re coming from &lt;strong&gt;Spring Boot&lt;/strong&gt; or other traditional Java frameworks, this comprehensive guide will help you understand what Quarkus brings to the table and whether it’s the right choice for your next project.&lt;/p&gt;
&lt;p&gt;Sample project link with a readme: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/rajkundalia/url-shortener-quarkus&quot;&gt;https://github.com/rajkundalia/url-shortener-quarkus&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kjvm6s8ei3f8mdezw5db.png&quot; alt=&quot;Gemini Generated Image&quot;&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;1-introduction-to-quarkus&quot;&gt;1. Introduction to Quarkus&lt;/h3&gt;
&lt;p&gt;Quarkus is a &lt;strong&gt;Kubernetes-native Java stack&lt;/strong&gt; tailored for OpenJDK HotSpot and GraalVM, crafted from the best-of-breed Java libraries and adhering to &lt;strong&gt;Jakarta EE&lt;/strong&gt; and &lt;strong&gt;MicroProfile&lt;/strong&gt; standards. Developed by Red Hat, it’s designed to make Java a leading platform in Kubernetes and serverless environments by dramatically reducing startup times and memory consumption.&lt;/p&gt;
&lt;p&gt;The “Supersonic Subatomic Java” tagline isn’t just marketing — it reflects Quarkus’s core promise:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Supersonic&lt;/strong&gt;: Lightning-fast startup times (under 100ms for many applications)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Subatomic&lt;/strong&gt;: Minimal memory footprint (as low as 12MB for simple applications)&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;key-benefits&quot;&gt;Key Benefits&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Performance&lt;/strong&gt;: Applications start in milliseconds rather than seconds, with memory usage reduced significantly compared to traditional frameworks. This translates to significant cost savings in cloud environments.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Developer Productivity&lt;/strong&gt;: Live coding capabilities, dev UI, and tooling make development faster and more enjoyable.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cloud-Native First&lt;/strong&gt;: Built with containers, Kubernetes, and serverless in mind. Quarkus generates Kubernetes manifests, Docker files, and provides native compilation support out of the box.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;target-use-cases&quot;&gt;Target Use Cases&lt;/h4&gt;
&lt;p&gt;Quarkus excels in:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Microservices architectures&lt;/li&gt;
&lt;li&gt;Serverless applications that need instant startup&lt;/li&gt;
&lt;li&gt;Cloud-native applications requiring efficiency&lt;/li&gt;
&lt;li&gt;High-throughput, low-latency systems leveraging reactive programming&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;What sets Quarkus apart is its &lt;strong&gt;compile-time optimization&lt;/strong&gt; approach. Unlike traditional frameworks (e.g., Spring Boot) that rely on runtime classpath scanning and reflection, Quarkus shifts this work to build time, eliminating overhead at runtime.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;2-core-architecture-amp-development-experience&quot;&gt;2. Core Architecture &amp;amp; Development Experience&lt;/h3&gt;
&lt;h4 id=&quot;dependency-injection&quot;&gt;Dependency Injection&lt;/h4&gt;
&lt;p&gt;Quarkus uses &lt;strong&gt;CDI (Contexts and Dependency Injection) 2.0&lt;/strong&gt;, specifically the &lt;strong&gt;ArC&lt;/strong&gt; implementation. All DI metadata is processed at build time:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@ApplicationScoped
public class GreetingService {
    public String greeting(String name) {
        return &quot;Hello &quot; + name;
    }
}

@Path(&quot;/hello&quot;)
public class GreetingResource {
    @Inject
    GreetingService service;

    @GET
    @Produces(MediaType.TEXT_PLAIN)
    public String hello() {
        return service.greeting(&quot;World&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;extension-ecosystem&quot;&gt;Extension Ecosystem&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;Extensions&lt;/strong&gt; augment application bytecode at build time, moving runtime operations to compile time. Popular extensions include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;quarkus-resteasy-reactive&lt;/code&gt; — REST endpoints&lt;/li&gt;
&lt;li&gt;&lt;code&gt;quarkus-hibernate-orm-panache&lt;/code&gt; — database operations&lt;/li&gt;
&lt;li&gt;&lt;code&gt;quarkus-smallrye-openapi&lt;/code&gt; — API documentation&lt;/li&gt;
&lt;li&gt;&lt;code&gt;quarkus-micrometer&lt;/code&gt; — metrics&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;configuration-management&quot;&gt;Configuration Management&lt;/h4&gt;
&lt;p&gt;Configuration with &lt;code&gt;application.properties&lt;/code&gt; is simple:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-properties&quot;&gt;# Database configuration
quarkus.datasource.db-kind=postgresql
quarkus.datasource.jdbc.url=jdbc:postgresql://localhost/mydatabase

# Profiles
%dev.quarkus.log.level=DEBUG
%test.quarkus.datasource.jdbc.url=jdbc:h2:mem:test
%prod.quarkus.log.level=WARN
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;hot-reloading-amp-live-coding&quot;&gt;Hot Reloading &amp;amp; Live Coding&lt;/h4&gt;
&lt;p&gt;Run &lt;code&gt;mvn quarkus:dev&lt;/code&gt; for instant updates:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Code recompiles automatically&lt;/li&gt;
&lt;li&gt;Config updates apply instantly&lt;/li&gt;
&lt;li&gt;Continuous testing runs automatically&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;devui&quot;&gt;DevUI&lt;/h4&gt;
&lt;p&gt;At &lt;code&gt;http://localhost:8080/q/dev/&lt;/code&gt;, you’ll find:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Extension management&lt;/li&gt;
&lt;li&gt;Database console&lt;/li&gt;
&lt;li&gt;OpenAPI browser&lt;/li&gt;
&lt;li&gt;Config editor&lt;/li&gt;
&lt;li&gt;Health checks &amp;amp; metrics&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;testing&quot;&gt;Testing&lt;/h4&gt;
&lt;p&gt;Testing is seamless with &lt;code&gt;@QuarkusTest&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@QuarkusTest
public class GreetingResourceTest {
    @Test
    public void testHelloEndpoint() {
        given()
            .when().get(&quot;/hello&quot;)
            .then()
            .statusCode(200)
            .body(is(&quot;Hello World&quot;));
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;3-performance-amp-technology-stack&quot;&gt;3. Performance &amp;amp; Technology Stack&lt;/h3&gt;
&lt;h4 id=&quot;jvm-vs-native-mode&quot;&gt;JVM vs Native Mode&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;JVM Mode&lt;/strong&gt;: Faster than Spring Boot (0.8–1.5s startup, 50–100MB memory), build time \~45s&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Native Mode&lt;/strong&gt;: GraalVM compilation gives &amp;lt;100ms startup, \~12–20MB memory, tiny container images, build time \~2–5 minutes [Not tried, obtained from Internet]&amp;lt;/li&amp;gt;
&lt;/li&gt;&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Trade-off&lt;/strong&gt;: Native builds take longer (2–5 minutes).&lt;/p&gt;
&lt;h4 id=&quot;performance-comparison&quot;&gt;Performance Comparison&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;Aspect &lt;/th&gt;&lt;th align=&quot;left&quot;&gt;Quarkus (JVM) &lt;/th&gt;&lt;th align=&quot;left&quot;&gt;Spring Boot&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;Startup Time &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;0.8–1.5s &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;2.5–4s&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;Memory Usage &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;50–100MB &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;250–500MB&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;Build Time &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;\~45s &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;\~30s&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;em&gt;Note: Please note, these are approximate numbers.&lt;/em&gt;&lt;/p&gt;
&lt;h4 id=&quot;technology-stack&quot;&gt;Technology Stack&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Eclipse Vert.x&lt;/strong&gt; → Reactive, event-driven foundation&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Eclipse MicroProfile&lt;/strong&gt; → Enterprise features (config, health, metrics, security, tracing)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;GraalVM Integration&lt;/strong&gt; → AOT compilation, dead code elimination, pre-initialized structures&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;4-cloud-native-amp-production-features&quot;&gt;4. Cloud-Native &amp;amp; Production Features&lt;/h3&gt;
&lt;h4 id=&quot;container-first&quot;&gt;Container-First&lt;/h4&gt;
&lt;p&gt;Quarkus auto-generates optimized Dockerfiles:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dockerfile&quot;&gt;FROM registry.access.redhat.com/ubi8/openjdk-11:1.3
COPY target/quarkus-app/ /deployments/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Native images shrink containers to under 100MB.&lt;/p&gt;
&lt;h4 id=&quot;kubernetes-integration&quot;&gt;Kubernetes Integration&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;Auto-generated manifests&lt;/li&gt;
&lt;li&gt;ConfigMaps &amp;amp; Secrets support&lt;/li&gt;
&lt;li&gt;Health/readiness probes&lt;/li&gt;
&lt;li&gt;Operator integration&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;observability&quot;&gt;Observability&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;Health endpoints&lt;/li&gt;
&lt;li&gt;Metrics via Micrometer/Prometheus&lt;/li&gt;
&lt;li&gt;Tracing with Jaeger/OpenTracing&lt;/li&gt;
&lt;li&gt;Structured logging&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;security&quot;&gt;Security&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;OIDC, OAuth2, JWT&lt;/li&gt;
&lt;li&gt;RBAC via annotations&lt;/li&gt;
&lt;li&gt;Keycloak integration&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;5-framework-comparisons&quot;&gt;5. Framework Comparisons&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;Aspect &lt;/th&gt;&lt;th align=&quot;left&quot;&gt;Quarkus &lt;/th&gt;&lt;th align=&quot;left&quot;&gt;Spring Boot&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Startup Time&lt;/strong&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;0.8–1.5s (JVM), &amp;lt;0.1s (Native) &amp;lt;/td&amp;gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;2.5–4s&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Memory Usage&lt;/strong&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;50–100MB (JVM), 10–30MB (Native) &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;250–500MB&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Architecture&lt;/strong&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Build-time optimized &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Runtime reflection/scanning&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Ecosystem&lt;/strong&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Growing, EE/MicroProfile aligned &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Mature, massive&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Native Compilation&lt;/strong&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Core feature &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Experimental (Spring Native)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Reactive&lt;/strong&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Built-in (Vert.x) &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Optional&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;em&gt;Please note, numbers can vary.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Choose Quarkus when you need &lt;strong&gt;performance&lt;/strong&gt;, &lt;strong&gt;microservices&lt;/strong&gt;, &lt;strong&gt;serverless&lt;/strong&gt;, or &lt;strong&gt;reactive programming&lt;/strong&gt;.&lt;/p&gt;
&lt;h4 id=&quot;quarkus-vs-micronaut&quot;&gt;Quarkus vs Micronaut&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Quarkus&lt;/strong&gt;: Build-time optimization, enterprise-ready (MicroProfile), Red Hat backing&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Micronaut&lt;/strong&gt;: Compile-time DI, small footprint, broader language support (Kotlin, Groovy)&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;essential-extensions&quot;&gt;Essential Extensions&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;quarkus-resteasy-reactive-jackson&lt;/code&gt; — REST + JSON&lt;/li&gt;
&lt;li&gt;&lt;code&gt;quarkus-hibernate-orm-panache&lt;/code&gt; — ORM&lt;/li&gt;
&lt;li&gt;&lt;code&gt;quarkus-jdbc-postgresql&lt;/code&gt; — Database&lt;/li&gt;
&lt;li&gt;&lt;code&gt;quarkus-smallrye-health&lt;/code&gt; — Health checks&lt;/li&gt;
&lt;li&gt;&lt;code&gt;quarkus-micrometer-registry-prometheus&lt;/code&gt; — Metrics&lt;/li&gt;
&lt;li&gt;&lt;code&gt;quarkus-container-image-docker&lt;/code&gt; — Container builds&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;learning-resources&quot;&gt;Learning Resources&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;Official Guides&lt;/li&gt;
&lt;li&gt;GitHub&lt;/li&gt;
&lt;li&gt;Community: Zulip, Stack Overflow, GitHub Discussions&lt;/li&gt;
&lt;li&gt;Red Hat training for enterprises&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://code.quarkus.io/?e=rest&quot;&gt;https://code.quarkus.io/?e=rest&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.logicmonitor.com/blog/quarkus-vs-spring&quot;&gt;https://www.logicmonitor.com/blog/quarkus-vs-spring&lt;/a&gt; — a good read&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://medium.com/sysco-labs/quarkus-a-supersonic-subatomic-java-e7c3ba510d79&quot;&gt;https://medium.com/sysco-labs/quarkus-a-supersonic-subatomic-java-e7c3ba510d79&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://maddevs.io/blog/spring-boot-vs-quarkus/&quot;&gt;https://maddevs.io/blog/spring-boot-vs-quarkus/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;Quarkus represents a &lt;strong&gt;significant evolution in Java development&lt;/strong&gt; — addressing performance bottlenecks while preserving Java’s strengths.&lt;/p&gt;
&lt;p&gt;With &lt;strong&gt;sub-second startups&lt;/strong&gt;, &lt;strong&gt;tiny memory usage&lt;/strong&gt;, and &lt;strong&gt;enterprise-ready features&lt;/strong&gt;, it’s tailor-made for microservices, serverless, and cloud-native apps. Backed by Red Hat and a growing community, Quarkus is positioned for long-term success.&lt;/p&gt;
&lt;p&gt;Whether starting fresh or migrating from Spring Boot, &lt;strong&gt;Quarkus deserves serious consideration for your next Java application.&lt;/strong&gt;&lt;/p&gt;
</description>
<category>Java</category>
<guid isPermaLink="true">https://coderlegion.com/5654/quarkus-revolutionizing-java-development-for-the-cloud-native-era</guid>
<pubDate>Fri, 26 Sep 2025 19:10:18 +0000</pubDate>
</item>
<item>
<title>Quarkus Neo4j: How to build a Quarkus app with Neo4j</title>
<link>https://coderlegion.com/3181/quarkus-neo4j-how-to-build-a-quarkus-app-with-neo4j</link>
<description>&lt;p&gt;I've recently been working on expanding my horizons in different Java frameworks, and Quarkus was on my list to try.&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://quarkus.io/about/&quot;&gt;Quarkus&lt;/a&gt; is a Kubernetes-native Java framework designed to reduce an application's footprint, ideal for addressing challenges in deploying, running, and maintaining applications in cloud-based environments.&lt;/p&gt;
&lt;p&gt;It has become a major player in list of frameworks, so naturally, I wanted to try it out! I started by following the first few steps in &lt;a rel=&quot;nofollow&quot; href=&quot;https://myfear.substack.com/p/the-power-of-relationships-neo4j-quarkus&quot;&gt;this blog post&lt;/a&gt;, as it outlines how to get a project up and running and the dependency needed for Neo4j. I felt the Quarkus Neo4j documentation was a bit overly complicated and verbose for a starter app.&lt;/p&gt;
&lt;p&gt;All of the code for this blog post is available in the &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/JMHReif/quarkus-coffee-shop&quot;&gt;quarkus-coffee-shop Github repository&lt;/a&gt;. &lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; I also have a &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/JMHReif/sdn-coffee-shop&quot;&gt;sister project using Spring Data Neo4j&lt;/a&gt;, for comparison. :)&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;creating-a-project&quot;&gt;Creating a project&lt;/h3&gt;
&lt;p&gt;Similar to the &lt;a rel=&quot;nofollow&quot; href=&quot;https://start.spring.io/&quot;&gt;Spring Initializr&lt;/a&gt; that offers a web interface for defining a project and its dependencies, Quarkus has &lt;a rel=&quot;nofollow&quot; href=&quot;https://code.quarkus.io/&quot;&gt;code.quarkus.io&lt;/a&gt;. Quarkus also offers a CLI, which would be especially helpful for creating multiple projects quickly.&lt;/p&gt;
&lt;p&gt;On code.quarkus.io, I filled out the project name, and then added three dependencies for the &lt;code&gt;Neo4j client&lt;/code&gt;, &lt;code&gt;RESTEasy Classic&lt;/code&gt;, and &lt;code&gt;RESTEasy Classic JSON-B&lt;/code&gt;. Once that is complete, click the &lt;code&gt;Generate your application&lt;/code&gt; blue button to download the .zip file.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=943366570968875687&quot; alt=&quot;Quarkus online code generator&quot;&gt;&lt;/p&gt;
&lt;p&gt;Next, unzip the folder and open it in your preferred IDE. The project template includes a &lt;code&gt;GreetingResource&lt;/code&gt; class with a default &lt;code&gt;/hello&lt;/code&gt; endpoint, so we can run the application and call the endpoint to see a message.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;./mvnw quarkus:dev

http &quot;:8080/hello&quot;

Hello RESTEasy
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;setup&quot;&gt;Setup&lt;/h3&gt;
&lt;p&gt;However, to build out our application, there are a couple of adjustments needed to the &lt;code&gt;pom.xml&lt;/code&gt;. To utilize object graph mapping capabilities, we need to include the &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/neo4j/neo4j-ogm-quarkus/releases/tag/3.14.0&quot;&gt;&lt;code&gt;neo4j-ogm-quarkus&lt;/code&gt;&lt;/a&gt; library, which currently (&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/neo4j/neo4j-ogm-quarkus/releases/tag/3.14.0&quot;&gt;v3.14.0&lt;/a&gt;) is only compatible with Quarkus 3.22.1.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; There is an open &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/neo4j/neo4j-ogm-quarkus/pull/482&quot;&gt;Github pull request for 3.23&lt;/a&gt;, which may be released soon.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;So, we need to downgrade Quarkus's platform version in order to use OGM (for now). Open the &lt;code&gt;pom.xml&lt;/code&gt;, then navigate to the &lt;code&gt;&amp;lt;properties&amp;gt;&lt;/code&gt; section and change the &lt;code&gt;quarkus.platform.version&lt;/code&gt; property to version &lt;code&gt;3.22.1&lt;/code&gt;, as shown below.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;properties&amp;gt;
    &amp;lt;quarkus.platform.version&amp;gt;3.22.1&amp;lt;/quarkus.platform.version&amp;gt;
&amp;lt;/properties&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next, add the following library to the &lt;code&gt;&amp;lt;dependencies&amp;gt;&lt;/code&gt; section.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.neo4j&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;neo4j-ogm-quarkus&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;3.14.0&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next, we need a Neo4j database and to connect to the instance from our application.&lt;/p&gt;
&lt;h3 id=&quot;creating-and-connecting-to-an-instance-of-neo4j&quot;&gt;Creating and connecting to an instance of Neo4j&lt;/h3&gt;
&lt;p&gt;If you don't have one already, spin up a &lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.neo4j.com/aura-java&quot;&gt;free cloud instance of Neo4j Aura&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Once the instance is running, load the data by clicking on the &lt;code&gt;Query&lt;/code&gt; tool along the left console menu, then copy the contents of the &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/JMHReif/graph-demo-datasets/blob/main/coffee-shop/load-data.cypher&quot;&gt;coffee shop import script&lt;/a&gt; and paste into the input box at the top of the window. Click the &lt;code&gt;play&lt;/code&gt; button on the right of the input box and wait for all steps to complete.&lt;/p&gt;
&lt;p&gt;Back in the Quarkus application, open the &lt;code&gt;src/main/resources/application.properties&lt;/code&gt; file to add configuration for connecting to Neo4j.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Neo4j config
quarkus.neo4j.uri=&amp;lt;NEO4J_URI&amp;gt;
quarkus.neo4j.authentication.username=&amp;lt;NEO4J_USERNAME&amp;gt;
quarkus.neo4j.authentication.password=&amp;lt;NEO4J_PASSWORD&amp;gt;

# OGM config
org.neo4j.ogm.use-native-types=true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The first three properties should match the URI, username, and password for the Neo4j instance. The final property is so that OGM will map temporal data types (dates and times) correctly.&lt;/p&gt;
&lt;p&gt;The next step is to create the domain classes for &lt;code&gt;Order&lt;/code&gt;, &lt;code&gt;Receipt&lt;/code&gt;, &lt;code&gt;Staff&lt;/code&gt;, and &lt;code&gt;Customer&lt;/code&gt; entities.&lt;/p&gt;
&lt;h3 id=&quot;domain-classes&quot;&gt;Domain classes&lt;/h3&gt;
&lt;p&gt;The domain model for this application contains 4 main entities and the relationships between them.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=12849477509161879787&quot; alt=&quot;Coffee shop graph data model&quot;&gt;&lt;/p&gt;
&lt;p&gt;We will map the &lt;code&gt;Order&lt;/code&gt; entity first. Create a new file named &lt;code&gt;Order.java&lt;/code&gt; and define the class as follows.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import java.time.LocalDate;
import java.time.LocalTime;

import org.neo4j.ogm.annotation.Id;
import org.neo4j.ogm.annotation.NodeEntity;
import org.neo4j.ogm.annotation.Relationship;

@NodeEntity
public class Order {
    @Id
    private String transactionId;
    private String orderId;
    private LocalDate orderDate;
    private LocalTime orderTime;
    private String inStore;

    //make OGM happy
    public Order() {
    }

    public Order(String transactionId) {
        this.transactionId = transactionId;
    }

    //getters and setters
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;@NodeEntity&lt;/code&gt; annotation is for OGM to map this class to an &lt;code&gt;Order&lt;/code&gt; node in Neo4j, and the &lt;code&gt;@Id&lt;/code&gt; annotation pinpoints the id field for the class. After the class variables for &lt;code&gt;orderId&lt;/code&gt;, etc, we have to create an empty constructor, as well as a required parameter constructor for OGM. Then, the getters and setters follow.&lt;/p&gt;
&lt;p&gt;Next, let's define the &lt;code&gt;Staff.java&lt;/code&gt; and &lt;code&gt;Customer.java&lt;/code&gt; classes.&lt;/p&gt;
&lt;p&gt;Staff.java:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@NodeEntity
public class Staff {
    @Id
    String staffId;
    String firstName;
    String lastName;
    LocalDate startDate;

    public Staff() {
    }

    public Staff(String staffId, String firstName, String lastName, LocalDate startDate) {
        this.staffId = staffId;
        this.firstName = firstName;
        this.lastName = lastName;
        this.startDate = startDate;
    }

    //getters and setters
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Customer.java:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@NodeEntity
public class Customer {
    @Id
    private String customerId;
    private String customerName;
    private String loyaltyId;

    public Customer() {
    }

    public Customer(String customerId, String customerName, String loyaltyId) {
        this.customerId = customerId;
        this.customerName = customerName;
        this.loyaltyId = loyaltyId;
    }

    //getters and setters
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;These classes are similar to &lt;code&gt;Order&lt;/code&gt;, so the code shouldn't contain any surprises.&lt;/p&gt;
&lt;h3 id=&quot;relationships&quot;&gt;Relationships&lt;/h3&gt;
&lt;p&gt;The next step is to define relationships. While we can directly define the relationship by adding a new variable to point to the other entity, this domain also contains relationship properties between &lt;code&gt;Order&lt;/code&gt; and &lt;code&gt;Customer&lt;/code&gt; that hold information on how many items the customer ordered and the total amount for the order.&lt;/p&gt;
&lt;p&gt;To map these properties, we need another class that holds the receipt information.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import org.neo4j.ogm.annotation.EndNode;
import org.neo4j.ogm.annotation.GeneratedValue;
import org.neo4j.ogm.annotation.Id;
import org.neo4j.ogm.annotation.RelationshipEntity;
import org.neo4j.ogm.annotation.StartNode;

@RelationshipEntity(&quot;BOUGHT&quot;)
public class Receipt {
    @Id @GeneratedValue
    private Long id;

    private Integer itemsInOrder;
    private Double orderTotal;

    @StartNode
    private Customer customer;

    @EndNode
    private Order order;

    //getters and setters
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;@RelationshipEntity&lt;/code&gt; maps this class to the &lt;code&gt;BOUGHT&lt;/code&gt; relationship in Neo4j. &lt;code&gt;@Id&lt;/code&gt; and &lt;code&gt;@GeneratedValue&lt;/code&gt; define the id and note that it's an internally-generated id. Two class variables hold the number of items and order total. Then, the &lt;code&gt;@StartNode&lt;/code&gt; and &lt;code&gt;@EndNode&lt;/code&gt; point to the starting and ending entities for the relationship.&lt;/p&gt;
&lt;p&gt;Finally, we can connect these pieces together by mapping the relationships from the &lt;code&gt;Order.java&lt;/code&gt; class.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@NodeEntity
public class Order {
    //previous class variables

    @Relationship(value = &quot;BOUGHT&quot;, direction = Relationship.Direction.INCOMING)
    private Receipt receiptAndCustomer;
    @Relationship(value = &quot;SOLD&quot;, direction = Relationship.Direction.INCOMING)
    private Staff staff;

    //constructors

    //getters and setters
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There are two definitions - one for each relationship. The &lt;code&gt;@Relationship&lt;/code&gt; annotation defines the relationship type (&lt;code&gt;BOUGHT&lt;/code&gt; or &lt;code&gt;SOLD&lt;/code&gt;) and the direction (both incoming to the &lt;code&gt;Order&lt;/code&gt; node). Those are mapped to class variables that return the entity type (&lt;code&gt;Receipt&lt;/code&gt; and &lt;code&gt;Staff&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;These definitions map connections from the &lt;code&gt;Order&lt;/code&gt; class to related entities. Now we need to define a repository and resource to retrieve entities from the database and create endpoints for us to access it.&lt;/p&gt;
&lt;h3 id=&quot;repository-class&quot;&gt;Repository class&lt;/h3&gt;
&lt;p&gt;Create an &lt;code&gt;OrderRepository.java&lt;/code&gt; and populate it with the following code:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import java.util.Map;
import org.neo4j.ogm.session.SessionFactory;
import jakarta.enterprise.context.ApplicationScoped;

@ApplicationScoped
public class OrderRepository {
    private final SessionFactory sessionFactory;

    OrderRepository(SessionFactory sessionFactory) {
        this.sessionFactory = sessionFactory;
    }
    
    Iterable&amp;lt;Order&amp;gt; findTenOrders() {
        return sessionFactory.openSession().query(Order.class, 
            &quot;MATCH (order:Order)&amp;lt;-[rel:BOUGHT]-(c:Customer), &quot; +
                &quot;(order)&amp;lt;-[rel2:SOLD]-(s:Staff) &quot; + 
                &quot;RETURN order, collect(rel), collect(c), collect(rel2), collect(s) LIMIT 10;&quot;, 
            Map.of());
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;a rel=&quot;nofollow&quot; href=&quot;https://quarkus.io/guides/cdi&quot;&gt;&lt;code&gt;@ApplicationScoped&lt;/code&gt; annotation&lt;/a&gt; will share this repository bean across the application. Inside the class, we inject the OGM &lt;code&gt;SessionFactory&lt;/code&gt; into the class to handle connections to Neo4j.&lt;/p&gt;
&lt;p&gt;Next, we define a method to find 10 orders. The query retrieves customers who bought orders, and staff who sold those orders. By returning the order nodes and collecting related entities, unique patterns (orders) are returned. OGM will map returned nodes and relationships accordingly.&lt;/p&gt;
&lt;p&gt;Finally, we need an endpoint to access this information.&lt;/p&gt;
&lt;h3 id=&quot;resource-i-e-controller-class&quot;&gt;Resource (i.e. Controller) class&lt;/h3&gt;
&lt;p&gt;Quarkus uses the term &lt;code&gt;Resource&lt;/code&gt; rather than &lt;code&gt;Controller&lt;/code&gt;, so the &lt;code&gt;OrderResource.java&lt;/code&gt; will handle the user interface by creating a REST api.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import jakarta.enterprise.context.RequestScoped;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;

@RequestScoped
@Path(&quot;/orders&quot;)
public class OrderResource {
    private final OrderRepository orderRepository;

    @Inject
    public OrderResource(OrderRepository orderRepository) {
        this.orderRepository = orderRepository;
    }

    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public Iterable&amp;lt;Order&amp;gt; getOrders() {
        return orderRepository.findTenOrders();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;First, the &lt;code&gt;@RequestScoped&lt;/code&gt; narrows this bean to each request, and the &lt;code&gt;@Path&lt;/code&gt; defines the root url of &lt;code&gt;/orders&lt;/code&gt;. Within the class, there is a variable for the repository and it's injected into the class constructor, so that we can call the query method we wrote earlier.&lt;/p&gt;
&lt;p&gt;Next, we define the &lt;code&gt;getOrders()&lt;/code&gt; method as a HTTP GET method that will return multiple orders (&lt;code&gt;Iterable&amp;lt;Order&amp;gt;&lt;/code&gt;) in JSON output &lt;code&gt;@Produces(MediaType.APPLICATION_JSON)&lt;/code&gt;. The method will call the repository's &lt;code&gt;findTenOrders()&lt;/code&gt; method.&lt;/p&gt;
&lt;p&gt;Now we can test the application!&lt;/p&gt;
&lt;h3 id=&quot;run-and-test-the-application&quot;&gt;Run and test the application&lt;/h3&gt;
&lt;p&gt;Run the application in dev mode with the following command:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;./mvnw quarkus:dev
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We can test the default &lt;code&gt;/hello&lt;/code&gt; endpoint (&lt;code&gt;GreetingResource&lt;/code&gt;), as well as the &lt;code&gt;/orders&lt;/code&gt; endpoint (&lt;code&gt;OrderResource&lt;/code&gt;), which retrieves 10 order entities (and their related entities) from Neo4j:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;http &quot;:8080/hello&quot;

http &quot;:8080/orders&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;wrapping-up-&quot;&gt;Wrapping up!&lt;/h3&gt;
&lt;p&gt;In this blog post, we walked through the following:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;How to create a Quarkus application using the Quarkus project online tool.&lt;/li&gt;
&lt;li&gt;How to connect to Neo4j.&lt;/li&gt;
&lt;li&gt;How to map domain entities to Neo4j with OGM.&lt;/li&gt;
&lt;li&gt;How to run queries in Neo4j and access the data via a REST endpoint.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This project is a jumping point into so many other facets, and I'll be sure to bring you along for the ride.&lt;/p&gt;
&lt;p&gt;Happy coding!&lt;/p&gt;
&lt;h3 id=&quot;resources&quot;&gt;Resources&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Github repository: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/JMHReif/quarkus-coffee-shop&quot;&gt;Accompanying code for this blog post&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Documentation: &lt;a rel=&quot;nofollow&quot; href=&quot;https://quarkus.io/about/&quot;&gt;What is Quarkus?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Data set: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/JMHReif/graph-demo-datasets/tree/main/coffee-shop&quot;&gt;Coffee shop graph data&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Documentation: &lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.quarkiverse.io/quarkus-neo4j/dev/index.html&quot;&gt;Quarkus Neo4j&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Github repository: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/neo4j/neo4j-ogm-quarkus&quot;&gt;Neo4j Quarkus OGM library&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
<category>Intro to DBs</category>
<guid isPermaLink="true">https://coderlegion.com/3181/quarkus-neo4j-how-to-build-a-quarkus-app-with-neo4j</guid>
<pubDate>Fri, 30 May 2025 20:23:12 +0000</pubDate>
</item>
</channel>
</rss>