People see a fluent Java DSL in Quarkus Flow and a fluent Java DSL in Apache Camel and assume the two projects are going after the same problem. They are not. One manages the lifecycle of a process — decisions, waits, retries, AI agents. The other moves data between systems. They solve different problems, and in a well-designed system you may find yourself using both.
This post breaks down why.
Two Different Questions
Apache Camel answers: “How do I get data from here to there, transforming it along the way?”
Quarkus Flow answers: “How do I manage a process that involves multiple steps, decisions, waits for external input, and coordination of services and agents?”
Camel is an integration framework built around Enterprise Integration Patterns. Its primary value proposition is its connector ecosystem — 300+ adapters for every message broker, database, cloud service, and protocol you are likely to encounter. A Camel route picks up a message, transforms it, and delivers it. The route itself is stateless. Each message is independent. There is no concept of “where is this process right now.”
Quarkus Flow is a workflow orchestration engine. Think of it as a conductor: it knows the full score, coordinates who acts when, handles pauses that can last hours or days, and ensures the workflow reaches its conclusion — even after a server restart. A workflow instance has a persistent identity. You can ask what state instance #4217 is in right now, and get a meaningful answer.
| Quarkus Flow | Apache Camel | |
|---|---|---|
| What it does | Orchestrates a process (steps, decisions, waits, retries) | Routes and transforms messages between systems |
| Core pattern | Orchestration (central coordinator) | Integration (pipes and filters) |
| Specification | Open Workflow (CNCF sandbox) | Enterprise Integration Patterns |
| State | Stateful workflow instances (persist, suspend, resume) | Stateless message routes |
| AI support | First-class agentic workflow orchestration | Single-step LLM connectors (no agent orchestration) |
| Connectors | Delegates to Quarkus ecosystem | 300+ connectors (its primary value proposition) |
The DSL Similarity Is Superficial
Both projects use a fluent Java DSL, which is where the confusion starts. But look at what each snippet actually expresses:
// Apache Camel: defines a message ROUTE
from("kafka:orders")
.transform().jsonpath("$.items")
.split(body())
.to("jms:queue:warehouse");
// Quarkus Flow: defines a WORKFLOW with tasks, decisions, and wait states
workflow("content-review")
.tasks(
agent("drafter", drafterAgent::draft),
agent("critic", criticAgent::critique),
listen("waitHumanReview", toOne("review.done").first()),
switchWhenOrElse(needsRevision(), "drafter", "publish", HumanReview.class),
emitJson("publish", "org.acme.draft.published")
).build();
The Camel snippet describes how a message moves between systems: from Kafka, transform, split, to JMS. It executes and forgets. The Quarkus Flow snippet describes how a workflow evolves: run an AI agent, wait for a human reviewer, branch based on their decision. The instance persists across all of that.
Shared language conventions do not imply shared purpose.
An Orchestration Problem vs. an Integration Problem
Here is a concrete content review pipeline to make the difference tangible:
- An AI agent drafts content based on a prompt.
- A second AI agent critiques the draft.
- The workflow pauses and publishes a “review needed” event, waiting for a human reviewer.
- The reviewer approves or requests revisions.
- If revisions are needed, the loop runs again. If approved, the content goes downstream.
This is an orchestration problem. The process has a beginning and an end. It branches. It has wait states that span hours or days. It has a persistent identity — you need to know what happened to review request #4217. No message router gives you that.
Now consider an order ingestion pipeline:
- An order arrives via REST in JSON format.
- Transform it to XML.
- Route it to a JMS queue for the warehouse system.
- Copy it to an S3 bucket for auditing.
- If the amount exceeds a threshold, notify Slack.
This is an integration problem. Each message is independent. There is no process instance to track. Camel’s content-based routing, splitting, aggregating, and dead-letter channel patterns are exactly what this needs. Quarkus Flow would be overbuilt here — tracking workflow identity, persisting instance state, and managing lifecycle overhead adds real cost that buys you nothing when messages are transient and throughput matters.
The conceptual distinction:
| Orchestration (Quarkus Flow) | Integration (Apache Camel) | |
|---|---|---|
| Central question | “Where is this process right now, and what happens next?” | “Where does this message need to go?” |
| Unit of work | A long-lived workflow instance with identity and state | A single message flowing through a pipeline |
| State management | Persists workflow state; resumes across restarts | Messages are in-flight; the route itself is stateless |
| Wait/pause | First-class: a workflow can wait days for a human decision | Not a concept: messages either flow or fail |
| Branching | Based on conditions, loops, retries | Based on message content or headers |
Extension Models That Reflect Different Purposes
Both Quarkus Flow and Camel Quarkus integrate into the Quarkus ecosystem as extensions, but their extension catalogs reflect their different missions.
Quarkus Flow’s extensions are all about workflow concerns:
| Module | Purpose |
|---|---|
quarkus-flow (core) | Workflow engine, Java DSL, YAML workflow support |
quarkus-flow-langchain4j | AI agent orchestration via LangChain4j |
quarkus-flow-messaging | CloudEvent bridge to Kafka/AMQP for event-driven workflows |
quarkus-flow-persistence-* | Workflow state persistence (JPA, Redis, Infinispan) |
quarkus-flow-durable-kubernetes | Kubernetes-native durable execution |
quarkus-flow-scheduler | Time-triggered workflow execution |
quarkus-flow-grpc | gRPC protocol support |
quarkus-flow-oidc | Security/identity integration |
Notice what is not there: connectors to external systems. When a workflow task needs to call an HTTP API, query a database, or send an email, it uses CDI-injected Quarkus beans — RESTEasy, Hibernate, Mailer. The engine orchestrates the work; the platform handles the connections. That separation is intentional.
Camel Quarkus’s extensions are primarily connectors:
| Extension pattern | Purpose |
|---|---|
camel-quarkus-core | Camel runtime adapted for Quarkus |
camel-quarkus-kafka, camel-quarkus-jms, camel-quarkus-aws-s3, … | Massive component ecosystem spanning every broker, database, and cloud service |
camel-quarkus-jackson, camel-quarkus-jaxb, … | Data format transformations |
camel-quarkus-direct, camel-quarkus-seda, … | Internal routing patterns |
The connector ecosystem is Camel’s reason for existing. It is extraordinarily good at this.
The AI Orchestration Divide
This is where the two technologies diverge most sharply, and where the distinction between orchestration and integration really matters.
Coordinating AI agents is an orchestration problem. When you build an agentic AI pipeline, you need:
- Sequencing: Agent A drafts, then Agent B reviews, then Agent C edits.
- Parallelism: Agents A, B, and C research simultaneously, then results are merged.
- Loops with exit conditions: An agent refines until a quality threshold is met.
- Conditional routing: Route to different agents based on content classification.
- Human-in-the-loop: Pause the entire pipeline, wait for human approval, then resume.
- State persistence: If the server restarts, the pipeline resumes where it left off.
- Memory management: AI agents need access to shared context and conversation history.
Every one of these is a workflow orchestration concern. They require a stateful engine that can park a process and resume it — not a message router.
Quarkus Flow has three levels of AI integration through LangChain4j.
Level 1: Call any LangChain4j AI service as a workflow task directly:
workflow("content-pipeline")
.tasks(
agent("writer", writerAgent::draft, String.class),
agent("editor", editorAgent::refine, String.class)
).build();
Level 2: Annotation-driven agentic topologies that compile into workflow definitions at build time:
// This annotation generates a complete Quarkus Flow workflow at build time
@SequenceAgent(subAgents = { CreativeWriter.class, AudienceEditor.class, StyleEditor.class })
String write(@V("topic") String topic);
@ParallelAgent(subAgents = { FoodExpert.class, MovieExpert.class })
List<EveningPlan> plan(@V("mood") String mood);
@LoopAgent(maxIterations = 5, subAgents = { StyleScorer.class, StyleEditor.class })
String refine(@V("story") String story); // loops until quality score >= 0.8
Level 3: Hybrid — combine annotation-generated agent topologies with broader workflow logic, including human approvals and event waits:
workflow("reviewed-content")
.tasks(
agent("aiPipeline", storyCreator::write, String.class),
emitJson("content.review.required", ContentReview.class),
listen("waitForHuman", toOne("content.review.done").first()),
switchWhenOrElse(approved, "publish", "aiPipeline", ReviewDecision.class),
consume("publish", publishService::publish, Content.class)
).build();
Apache Camel does have a native camel-langchain4j-chat component, so this is not purely a generic HTTP call:
from("direct:askAI")
.setBody(constant("Summarize this document"))
.to("langchain4j-chat:myModel?chatModel=#openAiModel")
.log("Response: ${body}");
That is meaningfully better than a raw HTTP call, and fair to acknowledge. But it is still a stateless message step. One prompt in, one response out, and the route moves on. Camel has no concept of agent memory that persists across invocations, no loop-until-quality-met pattern, no mechanism to pause a route for days while a human reviews the output, and no way to resume it exactly where it left off after a restart. The component handles the LLM call; it does not orchestrate agents.
When to use each for AI scenarios:
| Scenario | Quarkus Flow | Apache Camel |
|---|---|---|
| Multi-agent content pipeline (draft, critique, revise, human approval) | Native support: sequential agents, loop-until-quality, HITL pause/resume, persistent state | Not designed for this |
| RAG ingestion pipeline (fetch, chunk, embed, store) | Can orchestrate the steps, Quarkus ecosystem handles connectors | Natural fit: ETL-style pipeline, connectors for S3/DB/HTTP |
| Agentic customer support (classify, route, escalate to human) | Native support: conditional agents, human escalation, conversation state | Can route a single request; cannot manage multi-turn state |
| Batch AI processing (10,000 documents through an LLM) | Works, but not its sweet spot | Better fit: splitter/aggregator, throttling, back-pressure |
| Real-time AI event enrichment (enrich each Kafka event with an LLM call) | Possible but heavy | Natural fit: stateless message transformation |
They Complement Each Other
Nothing stops you from using both in the same service.
A Quarkus Flow workflow orchestrates the process — draft, review, approve, publish. For the delivery step, it delegates to Camel via an injected ProducerTemplate:
@ApplicationScoped
public class PublishingWorkflow extends Flow {
@Inject
ProducerTemplate camel; // Camel's entry point
@Override
public Workflow descriptor() {
return FlowWorkflowBuilder.workflow("publish-content")
.tasks(
// Flow handles the stateful AI generation
agent("aiWriter", writerAgent::draft, String.class),
// Flow delegates the stateless integration to Camel
consume("uploadToS3", (String content) -> camel.sendBody("direct:s3-upload-route", content), String.class)
).build();
}
}
The workflow decides when to publish and what to publish. Camel’s direct:s3-upload-route handles the connector plumbing — marshaling, S3 upload, SNS notification, whatever the route defines. The failure boundary stays clean too: if Camel exhausts its redelivery policy and throws, the exception surfaces back into the Flow instance, where a workflow-level retry policy, compensation task, or error branch can handle it. The workflow provides the “when” and “why.” Camel provides the “how to connect.”
That is not a compromise. That is the right separation of concerns.
The Java DSL is a coincidence of shared language conventions. The purpose could not be more different.
Resources: