LangChain4j is a top (if not the top) library in use for integrating Java applications with various LLMs (large language models). It provides a number of features such as a unified API for LLM integration, support for vector stores, prompt templates, RAG (retrievalaugmented generation) and more. While we can use it directly in popular frameworks like Quarkus and Spring there are more specific extensions for those frameworks that provide for a simplified use of the various utilities provuded by the library.
The choice of Quarkus as a modern cloud-native framework in this article is not random: the framework already provides a huge ecosystem of extension and is being active used in more and more modern Java applicaations. And it really brings developer joy with all nice goodies like fast dev mode with hot reload, dev UI, dev services, unified configuration to name a few essential ones.
In this brief article we will discuss how the Quarkus LangChain4j extensions simplifies further use of the library and how we can use it to build an agentic application.
So why a separate extension for LangChain4j ?
There are several primary reasons to have a separate extension:
- the Quarkus architecture makes heavy use of build time optimizations including the extension mechanism so that extensions can be optimized for build time optimizations and native-image builds
- better integration using CDI beans (Quarkus uses a deicated dependency injection mechanism called ArC as a CDI implementation) in the form of a additional @AiService annotation for autodiscovery of AI services
- type-safe configuration for LangChain4j as supported by Quarkus using specific configuration classes for LangChain4j allowing user to define configuration such as:
quarkus.langchain4j.openai.api-key
quarkus.langchain4j.chat-model
- integration with Quarkus dev mode (i.e. for hot reload of configuration, Dev UI extension);
- sensible default observability configuration, including one for OpenTelemetry and tracing of calls to LLMs
All of this sounds really great, not only a simple plugin for Quarkus but a highly optimized and simplified use of the LangChain4j framework !
Now let’s just demonstrate how we can quick start using it with a practical example: a simple agentic healthcare application that given a description of medical conditions gives a prediction on which physician should be visited within a target hospital. The hospital stores an information in form of a table that maps a specific disease to a doctor that most expertise in that disease within the hospital. Let’s call it DocPredict.
DocPredict: THE High-Level Architecture
So straight to the point, let’s define how our simple healthcare assistant looks like from a high level perspective and build it:
Ideally if we want to build a complex frontend we would prefer to implement the DocPredict Web application using a modern favascript framework like React, Vue.js or AngularJS but in our case we will use a server-side rendering engine called Qute provided by Quarkus. Our service will interact with an OpenAI model to make the proper prediction based on a supplied definition and then try to map the respose to a proper doctor based on the existing hospital database. To not overcomplicate matters the database will be simply a CSV file with disease -> doctor pairs.
DocPredict: The code
To quickly bootstrap our initial application we can navigate to https://code.quarkus.io/ and supply proper configuration which is as simple as providing the basic Maven configuration and adding the following extensions:
- REST
- REST Jackson
- SmallRye OpenAPI (for Swagger)
- LangChain4j OpenAI
- Qute Web (for server-side templates)
Then download the generated application, unzip it and import it as a Maven project in your favourite IDE. In addition add the following library (we will use it to parse the CSV file with disease -> doctor records):
<dependency>
<groupId>com.opencsv</groupId>
<artifactId>opencsv</artifactId>
<version>5.7.1</version>
</dependency>
Next generate an OpenAI key from https://platform.openai.com/api-keys if you don’t have one already and add it to the application.properties file of the project as follows:
quarkus.langchain4j.chat-model.provider=openai
quarkus.langchain4j.openai.api-key=<key>
If we have multiple providers on the classpath (i.e. OpenPI and Gemini) we need to specify explicitly in the configuration which provider we want to use. In case you don’t have an API credit with a credit for OpenAI (unless you use a trial account with small initial credit) you can use a the free DeepSeek model via the OpenRouter platform for example: https://openrouter.ai/ Generate an API key there and use it as follows (the DeepSeek API is compatible with OpenAI):
quarkus.langchain4j.openai.api-key=<openrouter_api_key>
quarkus.langchain4j.openai.base-url=https://openrouter.ai/api/v1
quarkus.langchain4j.openai.model=deepseek/deepseek-r1:free
In the src/main/resources folder let’s create a doctors.csv file with the following content:
diabetes,Dr. Smith,Endocrinologist
hypertension,Dr. Johnson,Cardiologist
asthma,Dr. Lee,Pulmonologist
migraine,Dr. Patel,Neurologist
arthritis,Dr. Brown,Rheumatologist
pneumonia,Dr. Davis,Internist
depression,Dr. Wilson,Psychiatrist
allergy,Dr. Clark,Allergist
kidney Stones,Dr. Lewis,Urologist
anemia,Dr. Hall,Hematologist
Now let’s add logic for our application. First we will create an AI service with a prompt template that is used to interact with the LLM:
package com.javaadvent.docpredict;
import dev.langchain4j.service.SystemMessage;
import dev.langchain4j.service.UserMessage;
import io.quarkiverse.langchain4j.RegisterAiService;
import jakarta.enterprise.context.ApplicationScoped;
@RegisterAiService
@SystemMessage(“You are a professional doctor”)
@ApplicationScoped
public interface DiseasePredictionService {
@UserMessage(“””
Try to predict disease from the following list of symptoms: {symptoms}.
Return only one predicted disease as a single word without extra symbols in lowercase.
“””)
String predictDisease(String symptoms);
}
The @RegisterAiService is the essential annotation provided by the Quarkus LangChain4j extension that defines a service that interacts with the LLM. We also provide a system message to the LLM using the @SystemMessage annotation as an additional context. We define the predictDisease method that uses a prompt template defined by the @UserMessage annotation.
Using that service now let’s create a REST resource that is able to make proper predictions:
package com.javaadvent.docpredict;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.HashMap;
import org.jboss.logging.Logger;
import com.opencsv.CSVReader;
import com.opencsv.exceptions.CsvValidationException;
import jakarta.inject.Inject;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
@Path(“/predict”)
public class PredictionResource {
private static final Logger LOGGER = Logger.getLogger(PredictionResource.class);@Inject
private DiseasePredictionService predictionService;
@POST
@Produces(MediaType.APPLICATION_JSON)
public PredictionResponse predictDisease(PredictionRequest request) {
PredictionResponse response = new PredictionResponse();
String predictedDisease = predictionService.predictDisease(request.getSymptoms());
response.setDisease(predictedDisease);
response.setDoctor(determineDoctor(predictedDisease));
return response;
}
private String determineDoctor(String predictedDisease) {
InputStream is = PredictionResource.class.getResourceAsStream(“/doctors.csv”);
if (is == null) {
throw new IllegalStateException(“CSV file not found”);
}
HashMap<String, String> diseaseToDoctor = new HashMap<>();
try (BufferedReader br = new BufferedReader(new InputStreamReader(is));
CSVReader csvReader = new CSVReader(br)) {
String[] row;
while ((row = csvReader.readNext()) != null) {
diseaseToDoctor.put(row[0], String.format(“%s (%s)”, row[1], row[2]));
}
} catch (IOException | CsvValidationException e) {
LOGGER.error(e.getMessage(), e);
}
String doctor = diseaseToDoctor.get(predictedDisease);
if(doctor == null) {
doctor = “Visit general practitioner”;
}
return doctor;
}
}
package com.javaadvent.docpredict;
public class PredictionRequest {
private String symptoms;
public String getSymptoms() {return symptoms;
}
public void setSymptoms(String symptoms) {this.symptoms = symptoms;
}
}
package com.javaadvent.docpredict;
public class PredictionResponse {
private String disease;
private String doctor;
public String getDisease() {return disease;
}
public void setDisease(String disease) {this.disease = disease;
}
public String getDoctor() {
return doctor;
}
public void setDoctor(String doctor) {
this.doctor = doctor;
}
}
We inject the AI service that makes the call to the model API to make the prediction and then based on the result and the CSV file with disease -> doctor mappings tries to determine which practitioner is best suited for the described symptoms. If a practitioner cannot be suggested the application recommends visiting the general practitioner.
At that point we are ready to test the endpoint using Swagger when we start the Quarkus application and navigate to http://localhost:8080/q/swagger-ui/
Finally let’s make this a bit more convenient by adding a simple UI around it using Quarkus Qute template engine.
Create the following templates under src/main/resources/templates:
index.qute.html
<!DOCTYPE html>
<html>
<head>
<title>DocPredict</title>
</head>
<body>
<h1>Enter your symptoms</h1>
<form id=“symptomsForm“>
<input type=“text“ name=“symptoms“ placeholder=“Your symptoms“ required>
<button type=“submit“>Suggest specialist</button>
</form>
<div id=“prediction“></div>
<script>
const form = document.getElementById(‘symptomsForm’);
const resultDiv = document.getElementById(‘prediction’);
form.addEventListener(‘submit’, async (e) => {
e.preventDefault(); // prevent full page reload
const formData = new FormData(form);
const response = await fetch(‘/index/submit’, {
method: ‘POST’,
body: new URLSearchParams(formData)
});
// insert HTML fragment into page
const html = await response.text();
resultDiv.innerHTML = html;
});
</script>
</body>
</html>
prediction.qute.html
<p>{doctor} !</p>
Add the following endpoint to handle the template rendering:
package com.javaadvent.docpredict;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.HashMap;
import org.jboss.logging.Logger;
import com.opencsv.CSVReader;
import com.opencsv.exceptions.CsvValidationException;
import io.quarkus.qute.Template;
import io.quarkus.qute.TemplateInstance;
import jakarta.inject.Inject;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
@Path(“index”)
public class IndexResource {
private static final Logger LOGGER = Logger.getLogger(IndexResource.class);
@Inject
private Template index; // index.qute.html
@Inject
private Template prediction; // prediction.qute.html
@Inject
private DiseasePredictionService predictionService;
@GET
@Produces(MediaType.TEXT_HTML)
public TemplateInstance getForm() {
return index.instance();
}
@POST
@Path(“submit”)
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
@Produces(MediaType.TEXT_HTML)
public TemplateInstance submit(@FormParam(“symptoms”) String symptoms) {
String predictedDisease = predictionService.predictDisease(symptoms);
return prediction.data(“doctor”, determineDoctor(predictedDisease));
}
private String determineDoctor(String predictedDisease) {
InputStream is = PredictionResource.class.getResourceAsStream(“/doctors.csv”);
if (is == null) {
throw new IllegalStateException(“CSV file not found”);
}
HashMap<String, String> diseaseToDoctor = new HashMap<>();
try (BufferedReader br = new BufferedReader(new InputStreamReader(is));
CSVReader csvReader = new CSVReader(br)) {
String[] row;
while ((row = csvReader.readNext()) != null) {
diseaseToDoctor.put(row[0], String.format(“%s (%s)”, row[1], row[2]));
}
} catch (IOException | CsvValidationException e) {
LOGGER.error(e.getMessage(), e);
}
String doctor = diseaseToDoctor.get(predictedDisease);
if(doctor == null) {
doctor = “Visit general practitioner”;
}
return doctor;
}
}
And there we go once we navigate to http://localhost:8080/indexlocalhost:8080/index:
Summary
As you can see it is quite straight-forward to get started with Quarkus LangChain4j extension and furthermore you can start more complex capabilities in your application like RAG, context memory, tool support etc.






steinhauer.software