LangChain4j Introduction & Architecture
A complete LangChain4j introduction for Java developers: core abstractions, the AiServices declarative style, memory, tools and retrieval — plus an honest comparison with Spring AI.
On this page
LangChain4j takes a different route to the same destination as Spring AI. Where Spring AI gives you a fluent client you call, LangChain4j lets you declare an interface and generates the implementation. Both work. The declarative style is unusually pleasant once it clicks.
Hello, AiServices
import dev.langchain4j.model.chat.ChatModel;
import dev.langchain4j.model.openai.OpenAiChatModel;
import dev.langchain4j.service.AiServices;
// 1. Declare what you want as an ordinary Java interface.
interface Assistant {
String chat(String message);
}
public class Demo {
public static void main(String[] args) {
ChatModel model = OpenAiChatModel.builder()
.apiKey(System.getenv("OPENAI_API_KEY"))
.modelName("gpt-4o-mini")
.temperature(0.2)
.build();
// 2. LangChain4j implements the interface against the model.
Assistant assistant = AiServices.create(Assistant.class, model);
// 3. Call it like any other bean.
System.out.println(assistant.chat("Explain Java records in two sentences."));
}
}There is no prompt assembly, no response parsing and no HTTP client. The interface is the contract.
The architecture
Five concepts carry the library:
| Concept | What it does |
|---|---|
ChatModel | Talks to a provider. Swappable; the rest of your code does not change. |
ChatMemory | Holds conversation history within a bounded window. |
ContentRetriever | Fetches relevant context — usually from an EmbeddingStore. |
@Tool | A method the model may invoke. |
AiServices | Wires the above into a proxy behind your interface. |
Prompts as annotations
import dev.langchain4j.service.SystemMessage;
import dev.langchain4j.service.UserMessage;
import dev.langchain4j.service.V;
interface SupportAssistant {
@SystemMessage("""
You are a support agent for the Acme billing product.
Answer only questions about Acme billing.
Never invent prices, policies or account details.
""")
String answer(@UserMessage String question);
@SystemMessage("You extract structured data. Output nothing but the requested fields.")
@UserMessage("Extract the customer's intent from: {{message}}")
Intent classify(@V("message") String message);
}The prompt lives next to the method that uses it, which — unlike a prompt buried in a string constant three classes away — means it gets reviewed when the method changes.
Typed return values
This is where the declarative style earns its keep. The return type drives the parsing.
enum Sentiment { POSITIVE, NEUTRAL, NEGATIVE }
record Ticket(String summary, Priority priority, List<String> components) {
enum Priority { LOW, MEDIUM, HIGH, CRITICAL }
}
interface Analyst {
// Returns the enum directly. LangChain4j constrains the model to the
// permitted values, so you cannot receive "VERY_POSITIVE".
Sentiment sentimentOf(String review);
// Returns a record. The JSON schema is derived from the type.
Ticket triage(String bugReport);
// Even booleans work, which makes routing logic trivial.
boolean isSpam(String message);
}Analyst analyst = AiServices.create(Analyst.class, model);
if (analyst.isSpam(message)) {
return;
}
Ticket ticket = analyst.triage(message);Compare that with parsing free text into a Ticket by hand, with retries for malformed JSON. The
library does the schema derivation, the instruction and the deserialisation.
Memory
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
import dev.langchain4j.service.MemoryId;
interface Assistant {
// @MemoryId keeps each user's conversation separate.
String chat(@MemoryId String userId, @UserMessage String message);
}
Assistant assistant = AiServices.builder(Assistant.class)
.chatModel(model)
// A memory per user, each bounded to 20 messages. Without a bound the
// prompt grows on every turn until it exceeds the context window —
// and you pay for the entire history on every single call.
.chatMemoryProvider(userId -> MessageWindowChatMemory.withMaxMessages(20))
.build();Tools
import dev.langchain4j.agent.tool.Tool;
class BookingTools {
private final BookingRepository bookings;
BookingTools(BookingRepository bookings) {
this.bookings = bookings;
}
@Tool("Look up a booking by its reference. Returns the booking details or NOT_FOUND.")
String getBooking(String reference) {
return bookings.findByReference(reference)
.map(Booking::describe)
.orElse("NOT_FOUND: no booking with reference " + reference);
}
@Tool("Cancel a booking. Only call this after the customer has explicitly confirmed.")
String cancelBooking(String reference) {
// Authorisation belongs here, in code — not in the prompt.
return bookings.cancel(reference) ? "CANCELLED" : "ERROR: could not cancel";
}
}Assistant assistant = AiServices.builder(Assistant.class)
.chatModel(model)
.tools(new BookingTools(repository))
// Cap the loop. A confused agent will otherwise retry a failing tool
// until it exhausts your budget.
.maxSequentialToolsInvocations(5)
.build();Full treatment in LangChain4j agents and tools.
Retrieval in three lines
import dev.langchain4j.rag.content.retriever.EmbeddingStoreContentRetriever;
import dev.langchain4j.store.embedding.inmemory.InMemoryEmbeddingStore;
// Ingest: split, embed, store.
EmbeddingStoreIngestor.builder()
.documentSplitter(DocumentSplitters.recursive(500, 100))
.embeddingModel(embeddingModel)
.embeddingStore(embeddingStore)
.build()
.ingest(documents);
// Retrieve: attach to the service and RAG happens on every call.
Assistant assistant = AiServices.builder(Assistant.class)
.chatModel(model)
.contentRetriever(EmbeddingStoreContentRetriever.builder()
.embeddingStore(embeddingStore)
.embeddingModel(embeddingModel)
.maxResults(5)
.minScore(0.6) // drop weak matches
.build())
.build();The retriever runs before every call, injects what it finds, and your interface method signature never changes. See LangChain4j retrievers and RAG.
Using it from Spring Boot
package com.javaagentic.demo.config;
import dev.langchain4j.model.chat.ChatModel;
import dev.langchain4j.model.openai.OpenAiChatModel;
import dev.langchain4j.service.AiServices;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class LangChain4jConfig {
@Bean
ChatModel chatModel(@Value("${OPENAI_API_KEY}") String apiKey) {
return OpenAiChatModel.builder()
.apiKey(apiKey)
.modelName("gpt-4o-mini")
.temperature(0.2)
.timeout(java.time.Duration.ofSeconds(60))
.maxRetries(3)
.build();
}
// Now injectable anywhere, like any other bean.
@Bean
Assistant assistant(ChatModel chatModel) {
return AiServices.builder(Assistant.class)
.chatModel(chatModel)
.chatMemoryProvider(id -> MessageWindowChatMemory.withMaxMessages(20))
.build();
}
}LangChain4j vs Spring AI, honestly
| LangChain4j | Spring AI | |
|---|---|---|
| Style | Declarative interfaces | Fluent client + advisors |
| Framework coupling | None required | Spring Boot |
| Configuration | Builders, or Spring/Quarkus starters | application.yml auto-config |
| Document loaders | Broader out of the box | Growing |
| Observability | Listener API, wire it up | Micrometer built in |
| Learning curve | Very low for the happy path | Very low if you know Spring |
| Ceremony for custom behaviour | More manual wiring | Advisors are a clean seam |
Choose Spring AI if you are inside Spring Boot and want configuration, metrics and testing to follow the conventions your team already uses.
Choose LangChain4j if you want framework independence, if the declarative style fits how you think, or if you need one of its integrations that Spring AI does not have yet.
Use both if it helps — this happens more often than either project's documentation suggests. A common arrangement is Spring AI for the request path (because it is already wired into your metrics and security) and LangChain4j for offline ingestion (because its document loaders cover more formats). They are ordinary libraries; nothing prevents it.
Next
- LangChain4j chat models
- LangChain4j agents and tools
- Building a RAG pipeline with Spring Boot — the same pattern in the other framework
Frequently Asked Questions
What is LangChain4j?
Is LangChain4j a port of Python LangChain?
LangChain4j or Spring AI?
Does LangChain4j work with Spring Boot?
Can LangChain4j run models locally?
Related tutorials
- LangChain4j Chat ModelsConfigure chat models in LangChain4j: OpenAI, Anthropic Claude, Google Gemini, Mistral and Ollama — with streaming, timeouts, retries and how to swap providers without touching your code.
- LangChain4j Chains & CompositionCompose multi-step LLM workflows in LangChain4j: sequential chains, routing by classification, and building custom chains from AI Services — when to chain and when a single call suffices.
- LangChain4j Document LoadersLoad documents into LangChain4j from files, URLs, S3, GitHub and more, and parse PDF, DOCX and HTML with Apache Tika — the ingestion front-end for any RAG pipeline in Java.
- LangChain4j Text SplittersChunk documents effectively in LangChain4j: the recursive splitter, chunk size and overlap tuning, splitting code and markdown, and why chunking is the highest-impact decision in RAG.