Let's get started with a Microservice Architecture with Spring Cloud:
Building Intelligent Document Processing with Apache Camel, Docling and LangChain4j
Last updated: July 30, 2026
1. Overview
An LLM can answer questions about documents’ content when they are in the context window. However, many real-world documents are stored in formats such as PDF, which may be difficult for AI applications to process directly. Docling converts these documents into a structured format that can be efficiently parsed.
In this tutorial, we’ll build a document processing system using Apache Camel, Docling, and LangChain4j. Also, we’ll define a simple question-answering API using an embedded Undertow server.
2. Problem Statement
Many real-world documents contain structural information such as headings, tables, and images, which is difficult to preserve during text extraction. As a result, AI pipelines may lose valuable context, reducing the quality of retrieval and accuracy of generated responses.
A structured representation helps address this problem by preserving the document’s organization and relationships. Structured formats such as Markdown, HTML, and JSON retain much of the document’s semantic structure, making them easier to process and interpret.
IBM Docling bridges this gap by converting unstructured or semi-structured documents into structured representations that downstream AI applications can efficiently consume. We can store the generated Markdown on disk. Later, an application can load it and include it in the prompt for the LLM. This avoids converting the same document for every request the application processes.
3. Project Setup
Let’s bootstrap a Java project using Apache Camel and LangChain4j.
3.1. Maven Dependencies
First, let’s add the camel-core, camel-main, and langchain4j dependencies to our pom.xml:
<dependency>
<groupId>org.apache.camel</groupId>
<artifactId>camel-core</artifactId>
<version>4.20.0</version>
</dependency>
<dependency>
<groupId>org.apache.camel</groupId>
<artifactId>camel-main</artifactId>
<version>4.20.0</version>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j</artifactId>
<version>1.11.8</version>
</dependency>
The camel-core dependency provides classes such as RouteBuilder for defining a camel route using the Java DSL, camel-main provides a lightweight runtime for standalone Camel applications, and langchain4j provides the core APIs for interacting with LLMs.
Next, let’s add camel-docling, camel-langchain4j-chat, and langchain4j-open-ai dependencies to our pom.xml:
<dependency>
<groupId>org.apache.camel</groupId>
<artifactId>camel-docling</artifactId>
<version>4.20.0</version>
</dependency>
<dependency>
<groupId>org.apache.camel</groupId>
<artifactId>camel-langchain4j-chat</artifactId>
<version>4.20.0</version>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-open-ai</artifactId>
<version>1.11.8</version>
</dependency>
Here, camel-docling integrates Apache Camel with a Docling server, allowing routes to submit documents for conversion into Docling’s structured document representation. Also, camel-langchain4j-chat enables Camel routes to send prompts to a LangChain4j chat model and process the generated response. The langchain4j-open-ai dependency provides the implementation needed to connect LangChain4j to OpenAI-compatible chat models.
All Apache Camel dependencies, including camel-docling, use the same version to ensure compatibility and avoid unexpected runtime errors.
Let’s also add the camel-undertow dependency to our pom.xml:
<dependency>
<groupId>org.apache.camel</groupId>
<artifactId>camel-undertow</artifactId>
<version>4.20.0</version>
</dependency>
It enables Camel to expose HTTP endpoints using an embedded Undertow server.
Finally, let’s create a directory named document in the project’s root directory and place the policies.pdf file inside it.
3.2. Docker Compose File
The easiest way to run Docling locally is by using docling-serve, which exposes Docling’s document conversion capabilities through a REST API. For simplicity, we run docling-serve in a Docker container.
First, let’s create a new file docker-compose.yml in our project root directory and add the following configuration:
services:
docling-serve:
image: quay.io/docling-project/docling-serve:latest
container_name: docling-serve
ports:
- "5001:5001"
environment:
- DOCLING_STORAGE_DIR=/data
- DOCLING_PORT=5001
volumes:
- ./data/docling:/data
restart: unless-stopped
The configuration starts a docling-serve container from the quay.io/docling-project/docling-serve image. If the image isn’t available locally, Docker automatically pulls it from the registry. It also maps port 5001 on the host to port 5001 inside the container, allowing our application to communicate with the Docling server.
Finally, let’s start the Docling server by running the following command:
$ docker compose up
3.3. Application Entry Point
Let’s create a CamelDoclingApplication class that serves as our application entry point:
public class CamelDoclingApplication {
public static void main(String[] args) throws Exception {
OpenAiChatModel model = OpenAiChatModel.builder()
.baseUrl("http://langchain4j.dev/demo/openai/v1")
.apiKey("demo")
.modelName("gpt-4o-mini")
.build();
Main main = new Main();
main.bind("chatModel", model);
main.run(args);
}
}
We create an OpenAiChatModel object configured to use the gpt-4o-mini model. Although OpenAI models normally require a valid API key, LangChain4j provides a public demo endpoint and API key for experimentation. We configure the client to use this endpoint by setting the baseUrl to the LangChain4j demo service. Alternatively, we could configure LangChain4j to communicate with a locally hosted model through Ollama or replace the demo endpoint with our own OpenAI API credentials.
The Main instance provides a lightweight runtime for standalone Camel applications. We bind the chat model to Camel’s registry under the name chatModel, that Camel components and routes can reference during message processing.
4. Converting a PDF Document to Markdown
Now that the Docling server is running, let’s create our first Camel route to convert policies.pdf into Markdown:
class ConversionRoute extends RouteBuilder {
@Override
public void configure() {
from("file:documents?include=.*\\.(pdf|docx|pptx|html|md)&noop=true&idempotent=true")
.routeId("document-analysis-workflow")
.log("Processing document: ${header.CamelFileName}")
.to("docling:CONVERT_TO_MARKDOWN?useDoclingServe=true&doclingServeUrl=http://localhost:5001&contentInBody=true")
.setProperty("convertedMarkdown", body())
.setHeader(Exchange.FILE_NAME, simple("${header.CamelFileName.replaceFirst('\\.[^.]+$', '')}.md"))
.to("file:output")
}
}
Here, we monitor the documents directory for supported document types. The noop=true option leaves the original file in place after processing, while idempotent=true prevents the same file from being processed more than once. Note that the Camel file component already provides the information that the Docling component needs to process the document. Although we could explicitly replace the message body with the document’s path, doing so would be redundant because the conversion works correctly without it.
Next, the docling:CONVERT_TO_MARKDOWN endpoint sends the document to the locally running docling-serve instance, which converts it into Markdown and returns the generated content in the message body.
Finally, we store the converted Markdown as an exchange property, rename the output file by replacing its original extension with .md, and use the Camel file component to write it in the output directory.
If there are multiple documents in the documents directory, Camel processes each one independently and generates a corresponding Markdown file in the output directory.
Let’s run our application by registering ConversionRoute with the Camel runtime:
Main main = new Main();
main.bind("chatModel", model);
main.configure()
.addRoutesBuilder(new ConversionRoute());
main.run(args);
Here, we register the ConversionRoute with Camel through the Main instance.
5. Passing the Markdown Document to LangChain4j
Now that we’ve converted the document to Markdown, let’s send it to our chat model and ask it to analyze the document:
class ConversionRoute extends RouteBuilder {
@Override
public void configure() {
from("file:documents?include=.*\\.(pdf|docx|pptx|html|md)&noop=true&idempotent=true")
.routeId("document-analysis-workflow")
// ...
.setBody(simple("""
You are a helpful document analysis assistant. Please analyze
the following document and provide:
1. A brief summary (2-3 sentences)
2. Key topics and main points
3. Any important findings or conclusions
Document content:
${exchangeProperty.convertedMarkdown}
"""))
.to("langchain4j-chat:analysis?chatModel=#chatModel")
.setHeader(
Exchange.FILE_NAME,
simple("${header.CamelFileName.replaceFirst('\\.[^.]+$', '')}-analysis.md")
)
.to("file:analysis");
}
}
In this route, we construct a prompt that includes the Markdown generated by Docling and instruct the model to summarize and analyze the document. The ${exchangeProperty.convertedMarkdown} expression inserts the Markdown stored earlier in the route into the prompt before it’s sent to the model.
Next, the langchain4j-chat component sends the prompt to the chat model referenced by #chatModel. Camel resolves this reference from its registry, where we previously bound the OpenAiChatModel instance. The analysis in langchain4j-chat:analysis is the endpoint name. It’s primarily used to distinguish this endpoint from others.
Finally, we rename the output file by appending -analysis to the original filename and use the File component to write the model’s response to the analysis directory.
6. Interactive Q&A API
Now, it’s time to define a question-answering API that uses the converted Markdown document as its knowledge source.
Let’s create a Camel route that accepts a question through an HTTP endpoint:
public class QuestionAndAnswerRoute extends RouteBuilder {
@Override
public void configure() throws Exception {
from("undertow:http://0.0.0.0:8080/api/ask?httpMethodRestrict=POST").routeId("document-qa-api")
.setProperty("question", bodyAs(String.class))
.pollEnrich("file:output?fileName=policies.md&noop=true&idempotent=false")
.setProperty("markdown", bodyAs(String.class))
.setBody(simple("""
You are a helpful document assistant.
Answer ONLY using the document below.
DOCUMENT
=========
${exchangeProperty.markdown}
QUESTION
=========
${exchangeProperty.question}
"""))
.to("langchain4j-chat:analysis?chatModel=#chatModel")
.setHeader(Exchange.CONTENT_TYPE, constant("text/plain"));
}
}
What happens here?
We expose the POST /api/ask endpoint through the embedded Undertow server. The request body contains the user’s question, which we store as an exchange property before replacing the message body later in the route.
The pollEnrich Enterprise Integration Pattern (EIP) reads the previously generated Markdown document from the output directory and places its contents into the current exchange. Rather than converting the PDF each time a request arrives, this route reads the Markdown file. This keeps the HTTP request lightweight by reusing the preprocessed document instead of invoking Docling for each question.
Also, we set idempotent=false so the File component can read the same Markdown file on every API request. By default, noop=true enables idempotent=true, which prevents the same file from being consumed more than once.
Then, we construct a prompt containing both the document and the user’s question before sending it to the langchain4j-chat component.
Finally, the model’s response is returned as the HTTP response body with a text/plain content type.
Let’s update our application entry point to register the QuestionAndAnswerRoute:
// ...
main.configure()
.addRoutesBuilder(new QuestionAndAnswerRoute());
// ...
Once the application is up, we can run the following curl command to send a question to the API:
curl -X POST \
http://localhost:8080/api/ask \
-H "Content-Type: text/plain" \
-d "How many lifecycle stages does a task move through?"
Here’s an API response we can expect:
A task moves through four lifecycle stages: PENDING, IN_PROGRESS, REVIEW, and DONE.
The API successfully answers the question based on the document we supplied.
7. Conclusion
In this article, we learned how to integrate Apache Camel, Docling, and LangChain4j to build an intelligent document processing application. We used Docling to convert an unstructured document into a structured representation, orchestrated the conversion workflow with Apache Camel, and analyzed the converted content using an LLM through LangChain4j.
Additionally, we exposed a simple question-answer API that uses the converted document as context to generate grounded responses.
By converting documents into a structured representation once and reusing the generated Markdown for subsequent interactions, we simplify document processing workflows, avoid repeated conversions, and make it easier for LLMs to process the document content.
As always, the source code for the examples is available over on GitHub.
















