Enterprise CDI extension for LangChain4j - inject AI services directly into your Jakarta EE and MicroProfile applications.
- Overview
- Quick Start
- Dependencies
- Configuration Reference
- AI Service Registration
- Tools and Function Calling
- RAG (Retrieval Augmented Generation)
- Chat Memory
- MicroProfile Integration
- Examples
- Troubleshooting
This project provides seamless integration between LangChain4j and CDI (Contexts and Dependency Injection), enabling you to:
- Inject AI services as CDI beans using
@RegisterAIService - Configure LLM components via properties (can use microprofile configuration adapter or provide your own)
- Add resilience with MicroProfile Fault Tolerance (
@Retry,@Timeout,@CircuitBreaker) - Monitor AI operations with MicroProfile Telemetry/OpenTelemetry
| Runtime | Extension Type |
|---|---|
| Quarkus | Build-compatible |
| Helidon | Both |
| WildFly | Portable |
| Payara | Portable |
| GlassFish | Portable |
| Liberty | Portable |
For portable extension (WildFly, Payara, GlassFish, Liberty):
<dependency>
<groupId>dev.langchain4j.cdi</groupId>
<artifactId>langchain4j-cdi-portable-ext</artifactId>
<version>${langchain4j-cdi.version}</version>
</dependency>For build-compatible extension (Quarkus, Helidon):
<dependency>
<groupId>dev.langchain4j.cdi</groupId>
<artifactId>langchain4j-cdi-build-compatible-ext</artifactId>
<version>${langchain4j-cdi.version}</version>
</dependency>For configuration using properties:
Provide your own LLMConfig SPI implementation (see Configuration Architecture),
or, if you use MicroProfile:
<dependency>
<groupId>dev.langchain4j.cdi</groupId>
<artifactId>langchain4j-cdi-config</artifactId>
<version>${langchain4j-cdi.version}</version>
</dependency>Add your LLM provider dependency:
<!-- For Ollama (local models) -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-ollama</artifactId>
<version>${langchain4j.version}</version>
</dependency>
<!-- For OpenAI -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-open-ai</artifactId>
<version>${langchain4j.version}</version>
</dependency>
<!-- For Anthropic Claude -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-anthropic</artifactId>
<version>${langchain4j.version}</version>
</dependency>For langchain4j-cdi-config create src/main/resources/META-INF/microprofile-config.properties,
for Quarkus add properties in application.properties.
# Chat model configuration (Ollama example)
dev.langchain4j.cdi.plugin.chat-model.class=dev.langchain4j.model.ollama.OllamaChatModel
dev.langchain4j.cdi.plugin.chat-model.config.base-url=http://localhost:11434
dev.langchain4j.cdi.plugin.chat-model.config.model-name=llama3.1
dev.langchain4j.cdi.plugin.chat-model.config.temperature=0.7dev.langchain4j.cdi.plugin.chat-model.class=dev.langchain4j.model.anthropic.AnthropicChatModel
dev.langchain4j.cdi.plugin.chat-model.config.api-key=${ANTHROPIC_API_KEY}
dev.langchain4j.cdi.plugin.chat-model.config.model-name=claude-sonnet-4-20250514
dev.langchain4j.cdi.plugin.chat-model.config.max-tokens=4096You can skip property-based configuration entirely and create your ChatModel using a CDI producer method:
import dev.langchain4j.model.chat.ChatModel;
import dev.langchain4j.model.anthropic.AnthropicChatModel;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.inject.Produces;
@ApplicationScoped
public class ChatModelProducer {
@Produces
@ApplicationScoped
public ChatModel chatModel() {
return AnthropicChatModel.builder()
.apiKey(System.getenv("ANTHROPIC_API_KEY"))
.modelName("claude-sonnet-4-20250514")
.maxTokens(4096)
.build();
}
}This approach gives you full programmatic control over the model configuration.
import dev.langchain4j.cdi.spi.RegisterAIService;
import dev.langchain4j.service.SystemMessage;
@RegisterAIService
public interface AssistantService {
@SystemMessage("You are a helpful assistant.")
String chat(String userMessage);
}import jakarta.inject.Inject;
import jakarta.ws.rs.*;
@Path("/assistant")
public class AssistantResource {
@Inject
AssistantService assistant;
@GET
@Path("/chat")
public String chat(@QueryParam("message") String message) {
return assistant.chat(message);
}
}| Module | Purpose |
|---|---|
langchain4j-cdi-core |
Core CDI integration classes |
langchain4j-cdi-portable-ext |
Runtime CDI extension |
langchain4j-cdi-build-compatible-ext |
Build-time CDI extension |
langchain4j-cdi-config |
MicroProfile Config integration |
langchain4j-cdi-fault-tolerance |
MicroProfile Fault Tolerance support |
langchain4j-cdi-telemetry |
OpenTelemetry metrics for AI operations |
<!-- For fault tolerance (@Retry, @Timeout, @CircuitBreaker) -->
<dependency>
<groupId>dev.langchain4j.cdi</groupId>
<artifactId>langchain4j-cdi-fault-tolerance</artifactId>
<version>${langchain4j-cdi.version}</version>
</dependency>
<!-- For OpenTelemetry metrics -->
<dependency>
<groupId>dev.langchain4j.cdi</groupId>
<artifactId>langchain4j-cdi-telemetry</artifactId>
<version>${langchain4j-cdi.version}</version>
</dependency>
<!-- To use microprofile configurations -->
<dependency>
<groupId>dev.langchain4j.cdi</groupId>
<artifactId>langchain4j-cdi-config</artifactId>
<version>${langchain4j-cdi.version}</version>
</dependency>LangChain4j CDI uses a Service Provider Interface (SPI) pattern for configuration, making it portable across different configuration systems.
At the core is the abstract LLMConfig class (dev.langchain4j.cdi.core.config.spi.LLMConfig). This class defines three abstract methods that any configuration provider must implement:
public abstract class LLMConfig {
public static final String PREFIX = "dev.langchain4j.cdi.plugin";
/** Initialize the configuration source (called once at startup) */
public abstract void init();
/** Return all property keys available in the configuration */
public abstract Set<String> getPropertyKeys();
/** Return the value for a given property key, or null if not found */
public abstract String getValue(String key);
}The LLMConfig implementation is discovered via Java ServiceLoader. You register your implementation in:
META-INF/services/dev.langchain4j.cdi.core.config.spi.LLMConfig
The langchain4j-cdi-config module provides LLMConfigMPConfig, an implementation that uses MicroProfile Config:
public class LLMConfigMPConfig extends LLMConfig {
private Config config;
@Override
public void init() {
config = ConfigProvider.getConfig();
}
@Override
public Set<String> getPropertyKeys() {
return StreamSupport.stream(config.getPropertyNames().spliterator(), false)
.filter(prop -> prop.startsWith(PREFIX))
.collect(Collectors.toSet());
}
@Override
public String getValue(String key) {
return config.getOptionalValue(key, String.class).orElse(null);
}
}This is registered in the service file:
# META-INF/services/dev.langchain4j.cdi.core.config.spi.LLMConfig
dev.langchain4j.cdi.core.mpconfig.LLMConfigMPConfig
When you add langchain4j-cdi-config to your dependencies, this implementation is automatically used.
You can create your own LLMConfig implementation for other configuration sources (YAML, database, etc.):
public class YamlLLMConfig extends LLMConfig {
private Map<String, String> properties;
@Override
public void init() {
// Load from YAML file
properties = loadYamlConfig("llm-config.yaml");
}
@Override
public Set<String> getPropertyKeys() {
return properties.keySet();
}
@Override
public String getValue(String key) {
return properties.get(key);
}
}Register it in META-INF/services/dev.langchain4j.cdi.core.config.spi.LLMConfig:
com.example.YamlLLMConfig
All LangChain4j components are configured using the following pattern:
dev.langchain4j.cdi.plugin.<bean-name>.class=<fully.qualified.ClassName>
dev.langchain4j.cdi.plugin.<bean-name>.scope=<scope-annotation> # Optional, defaults to @ApplicationScoped
dev.langchain4j.cdi.plugin.<bean-name>.config.<property>=<value>The <bean-name> becomes the CDI bean name (used with @Named qualifier).
The <property> is the property name camel cased converted to dashed text : logResponses -> log-responses.
dev.langchain4j.cdi.plugin.chat-model.class=dev.langchain4j.model.ollama.OllamaChatModel
dev.langchain4j.cdi.plugin.chat-model.config.base-url=http://localhost:11434
dev.langchain4j.cdi.plugin.chat-model.config.model-name=llama3.1
dev.langchain4j.cdi.plugin.chat-model.config.temperature=0.7
dev.langchain4j.cdi.plugin.chat-model.config.timeout=PT60S
dev.langchain4j.cdi.plugin.chat-model.config.log-requests=true
dev.langchain4j.cdi.plugin.chat-model.config.log-responses=truedev.langchain4j.cdi.plugin.chat-model.class=dev.langchain4j.model.openai.OpenAiChatModel
dev.langchain4j.cdi.plugin.chat-model.config.api-key=${OPENAI_API_KEY}
dev.langchain4j.cdi.plugin.chat-model.config.model-name=gpt-4
dev.langchain4j.cdi.plugin.chat-model.config.temperature=0.7
dev.langchain4j.cdi.plugin.chat-model.config.max-tokens=1000dev.langchain4j.cdi.plugin.chat-model.class=dev.langchain4j.model.azure.AzureOpenAiChatModel
dev.langchain4j.cdi.plugin.chat-model.config.endpoint=${AZURE_OPENAI_ENDPOINT}
dev.langchain4j.cdi.plugin.chat-model.config.api-key=${AZURE_OPENAI_KEY}
dev.langchain4j.cdi.plugin.chat-model.config.deployment-name=gpt-4dev.langchain4j.cdi.plugin.chat-model.class=dev.langchain4j.model.anthropic.AnthropicChatModel
dev.langchain4j.cdi.plugin.chat-model.config.api-key=${ANTHROPIC_API_KEY}
dev.langchain4j.cdi.plugin.chat-model.config.model-name=claude-sonnet-4-20250514
dev.langchain4j.cdi.plugin.chat-model.config.max-tokens=4096
dev.langchain4j.cdi.plugin.chat-model.config.temperature=0.7
dev.langchain4j.cdi.plugin.chat-model.config.log-requests=true
dev.langchain4j.cdi.plugin.chat-model.config.log-responses=true# MessageWindowChatMemory - keeps last N messages
dev.langchain4j.cdi.plugin.my-memory.class=dev.langchain4j.memory.chat.MessageWindowChatMemory
dev.langchain4j.cdi.plugin.my-memory.scope=jakarta.enterprise.context.ApplicationScoped
dev.langchain4j.cdi.plugin.my-memory.config.maxMessages=20dev.langchain4j.cdi.plugin.my-retriever.class=dev.langchain4j.rag.content.retriever.EmbeddingStoreContentRetriever
dev.langchain4j.cdi.plugin.my-retriever.config.embeddingStore=lookup:@default
dev.langchain4j.cdi.plugin.my-retriever.config.embeddingModel=lookup:@default
dev.langchain4j.cdi.plugin.my-retriever.config.maxResults=5
dev.langchain4j.cdi.plugin.my-retriever.config.minScore=0.7When a configuration property expects another CDI bean, use the lookup: prefix:
| Value | Description |
|---|---|
lookup:@default |
Use the default (unqualified) CDI bean |
lookup:@all |
Inject all beans as a List |
lookup:<bean-name> |
Use the bean with @Named("<bean-name>") |
@RegisterAIService(
scope = RequestScoped.class, // CDI scope (default: RequestScoped)
tools = {BookingTools.class}, // Tool classes for function calling
chatModelName = "chat-model", // Name of ChatModel bean (default: "#default")
chatMemoryName = "my-memory", // Name of ChatMemory bean
contentRetrieverName = "my-retriever", // Name of ContentRetriever bean (for RAG)
retrievalAugmentorName = "", // Alternative to contentRetriever
moderationModelName = "", // Name of ModerationModel bean
streamingChatModelName = "", // Name of StreamingChatModel bean
chatMemoryProviderName = "", // Name of ChatMemoryProvider bean
toolProviderName = "" // Name of ToolProvider bean
)
public interface MyAiService {
// ...
}| Attribute | Default | Description |
|---|---|---|
scope |
RequestScoped.class |
CDI scope for the AI service bean |
tools |
{} |
Array of CDI bean classes containing @Tool methods |
chatModelName |
"#default" |
Name of the ChatModel bean. "#default" uses the default bean |
chatMemoryName |
"" |
Name of ChatMemory bean (empty = no memory) |
contentRetrieverName |
"" |
Name of ContentRetriever bean for RAG |
retrievalAugmentorName |
"" |
Name of RetrievalAugmentor bean (alternative to contentRetriever) |
moderationModelName |
"" |
Name of ModerationModel bean for content moderation |
streamingChatModelName |
"" |
Name of StreamingChatModel bean for streaming responses |
chatMemoryProviderName |
"" |
Name of ChatMemoryProvider bean (for per-user memory) |
toolProviderName |
"" |
Name of ToolProvider bean (dynamic tool discovery) |
Use standard LangChain4j annotations on your AI service methods:
@RegisterAIService
public interface MyAiService {
@SystemMessage("You are a helpful assistant specialized in {{topic}}.")
@UserMessage("Answer this question: {{question}}")
String ask(@V("topic") String topic, @V("question") String question);
@SystemMessage("Summarize the following text.")
String summarize(@UserMessage String text);
}Tools enable your AI service to call your business logic.
import dev.langchain4j.agent.tool.P;
import dev.langchain4j.agent.tool.Tool;
import jakarta.enterprise.context.ApplicationScoped;
@ApplicationScoped
public class BookingTools {
@Tool("Get all bookings for a customer by their name")
public String getBookings(@P("Customer full name") String customerName) {
// Your business logic here
return "Booking #123: Rental car from 2024-01-15 to 2024-01-20";
}
@Tool("Cancel a booking by its ID")
public String cancelBooking(
@P("The booking ID to cancel") String bookingId,
@P("Customer name for verification") String customerName) {
// Your business logic here
return "Booking " + bookingId + " has been cancelled.";
}
}@RegisterAIService(tools = BookingTools.class)
public interface BookingAssistant {
@SystemMessage("""
You are a booking assistant for a car rental company.
Use the available tools to help customers with their bookings.
Always verify customer identity before making changes.
""")
String chat(String userMessage);
}@RegisterAIService(tools = {BookingTools.class, PaymentTools.class, NotificationTools.class})
public interface FullServiceAssistant {
String chat(String message);
}RAG enables your AI to answer questions based on your documents.
import dev.langchain4j.data.segment.TextSegment;
import dev.langchain4j.model.embedding.EmbeddingModel;
import dev.langchain4j.model.embedding.onnx.allminilml6v2.AllMiniLmL6V2EmbeddingModel;
import dev.langchain4j.store.embedding.EmbeddingStore;
import dev.langchain4j.store.embedding.inmemory.InMemoryEmbeddingStore;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.inject.Produces;
@ApplicationScoped
public class EmbeddingProducers {
@Produces
@ApplicationScoped
public EmbeddingModel embeddingModel() {
return new AllMiniLmL6V2EmbeddingModel();
}
@Produces
@ApplicationScoped
public EmbeddingStore<TextSegment> embeddingStore() {
return new InMemoryEmbeddingStore<>();
}
}dev.langchain4j.cdi.plugin.doc-retriever.class=dev.langchain4j.rag.content.retriever.EmbeddingStoreContentRetriever
dev.langchain4j.cdi.plugin.doc-retriever.config.embeddingStore=lookup:@default
dev.langchain4j.cdi.plugin.doc-retriever.config.embeddingModel=lookup:@default
dev.langchain4j.cdi.plugin.doc-retriever.config.maxResults=5
dev.langchain4j.cdi.plugin.doc-retriever.config.minScore=0.6@RegisterAIService(contentRetrieverName = "doc-retriever")
public interface DocumentAssistant {
@SystemMessage("Answer questions based on the provided context. If unsure, say so.")
String askAboutDocuments(String question);
}import dev.langchain4j.data.document.Document;
import dev.langchain4j.data.document.loader.FileSystemDocumentLoader;
import dev.langchain4j.data.document.splitter.DocumentSplitters;
import dev.langchain4j.data.segment.TextSegment;
import dev.langchain4j.model.embedding.EmbeddingModel;
import dev.langchain4j.store.embedding.EmbeddingStore;
import dev.langchain4j.store.embedding.EmbeddingStoreIngestor;
import jakarta.annotation.PostConstruct;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import java.util.List;
@ApplicationScoped
public class DocumentLoader {
@Inject
EmbeddingStore<TextSegment> embeddingStore;
@Inject
EmbeddingModel embeddingModel;
@PostConstruct
void loadDocuments() {
List<Document> documents = FileSystemDocumentLoader.loadDocuments("docs/");
EmbeddingStoreIngestor.builder()
.embeddingStore(embeddingStore)
.embeddingModel(embeddingModel)
.documentSplitter(DocumentSplitters.recursive(500, 50))
.build()
.ingest(documents);
}
}Chat memory maintains conversation context across multiple interactions.
dev.langchain4j.cdi.plugin.shared-memory.class=dev.langchain4j.memory.chat.MessageWindowChatMemory
dev.langchain4j.cdi.plugin.shared-memory.scope=jakarta.enterprise.context.ApplicationScoped
dev.langchain4j.cdi.plugin.shared-memory.config.maxMessages=50@RegisterAIService(chatMemoryName = "shared-memory")
public interface ChatBot {
String chat(String message);
}For multi-user scenarios, use a ChatMemoryProvider:
import dev.langchain4j.memory.ChatMemory;
import dev.langchain4j.memory.chat.ChatMemoryProvider;
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Named;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@ApplicationScoped
@Named("per-user-memory")
public class UserChatMemoryProvider implements ChatMemoryProvider {
private final Map<Object, ChatMemory> memories = new ConcurrentHashMap<>();
@Override
public ChatMemory get(Object memoryId) {
return memories.computeIfAbsent(memoryId,
id -> MessageWindowChatMemory.withMaxMessages(20));
}
}@RegisterAIService(chatMemoryProviderName = "per-user-memory")
public interface UserChatBot {
@SystemMessage("You are a helpful assistant.")
String chat(@MemoryId String sessionId, @UserMessage String message);
}Add resilience to AI operations:
<dependency>
<groupId>dev.langchain4j.cdi</groupId>
<artifactId>langchain4j-cdi-fault-tolerance</artifactId>
<version>${langchain4j-cdi.version}</version>
</dependency>import org.eclipse.microprofile.faulttolerance.*;
import java.time.temporal.ChronoUnit;
@RegisterAIService
public interface ResilientAssistant {
@Retry(maxRetries = 3, delay = 1000)
@Timeout(value = 30, unit = ChronoUnit.SECONDS)
@CircuitBreaker(requestVolumeThreshold = 10, failureRatio = 0.5)
@Fallback(fallbackMethod = "fallbackChat")
String chat(String message);
default String fallbackChat(String message) {
return "I'm temporarily unavailable. Please try again later.";
}
}Monitor AI operations with OpenTelemetry:
<dependency>
<groupId>dev.langchain4j.cdi</groupId>
<artifactId>langchain4j-cdi-telemetry</artifactId>
<version>${langchain4j-cdi.version}</version>
</dependency>Collected metrics:
gen_ai.client.token.usage- Input/output token countsgen_ai.client.operation.duration- Operation duration in seconds
All examples are based on a "Miles of Smiles" car rental company application inspired from the Java meets AI talk from Lize Raes at Devoxx Belgium 2023. Each example demonstrates:
- Chat Service: Customer assistant with RAG (Retrieval Augmented Generation)
- Fraud Detection: AI-powered fraud detection service
- Function Calling: Integration with business logic through tools
Complete example applications are available in the examples/ directory:
| Example | Runtime | Extension | Description |
|---|---|---|---|
payara-car-booking |
Payara Micro | Portable | Payara Micro with web UI |
liberty-car-booking |
Open Liberty | Portable | Liberty with OpenAPI UI |
liberty-car-booking-mcp |
Open Liberty | Portable | Liberty with MCP (Model Context Protocol) |
helidon-car-booking |
Helidon 4 | Build-compatible | Helidon with build-time extension |
helidon-car-booking-portable-ext |
Helidon 4 | Portable | Helidon with runtime extension |
quarkus-car-booking |
Quarkus | Build-compatible | Quarkus dev UI integration |
glassfish-car-booking |
GlassFish | Portable | Jakarta EE full profile |
Each example includes a run.sh script that starts Ollama (if needed) and the application server.
Quick start with any example:
cd examples/<example-name>
./run.shManual setup:
- Start Ollama:
# Using Docker/Podman
docker run -d --name ollama -p 11434:11434 -v ollama:/root/.ollama ollama/ollama
docker exec -it ollama ollama pull llama3.1
# Or install locally: https://ollama.ai/
ollama pull llama3.1- Run the example:
| Example | Command | Port |
|---|---|---|
payara-car-booking |
./run.sh |
8080 |
liberty-car-booking |
mvn liberty:dev |
9080 |
helidon-car-booking |
./run.sh |
8080 |
quarkus-car-booking |
./runexample.sh |
8080 |
glassfish-car-booking |
./run.sh |
8080 |
- Access the application:
- Payara: http://localhost:8080/
- Liberty: http://localhost:9080/openapi/ui
- Helidon: http://localhost:8080/openapi/ui
- Quarkus: http://localhost:8080/
- GlassFish: http://localhost:8080/glassfish-car-booking/api/car-booking/
Once running, you can ask questions like:
- "Hello, how can you help me?"
- "What is your cancellation policy?"
- "What is your list of cars?"
- "My name is James Bond, please list my bookings"
- "Is my booking 123-456 cancelable?"
Bean not found for ChatModel
- Ensure you have configured the chat model in
microprofile-config.properties - Check the bean name matches what you specified in
chatModelName
Configuration not loading
- Verify
microprofile-config.propertiesis insrc/main/resources/META-INF/ - Check property names match the builder method names (use kebab-case:
base-url,model-name)
Tool methods not being called
- Ensure tool class is a CDI bean (
@ApplicationScoped) - Check
@Tooldescription is clear for the LLM to understand when to use it - Verify tool class is listed in
@RegisterAIService(tools = ...)
Enable request/response logging:
dev.langchain4j.cdi.plugin.chat-model.config.log-requests=true
dev.langchain4j.cdi.plugin.chat-model.config.log-responses=trueSee CONTRIBUTING.md for contribution guidelines.
Apache License 2.0 - see LICENSE file.