LangChain4j Chains & Composition
Compose 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.
On this page
A chain is a workflow with a fixed shape: classify, then route, then format. Modern LangChain4j lets you build these by composing AI Services and ordinary Java, which keeps the control flow explicit — you can read it, test it, and step through it in a debugger.
Key Takeaways
- A chain is a fixed sequence you define; an agent decides its own steps. Prefer chains when the path is knowable.
- Build chains by composing AI Services with plain Java, not a magic Chain class.
- Mix models across steps — a cheap one to route, a strong one to reason.
- Chain only when there are genuinely distinct stages; a single call is cheaper and simpler.
A sequential chain
Each stage is an AI Service; plain Java wires them:
interface Summariser {
@UserMessage("Summarise the following in three sentences:\n\n{{it}}")
String summarise(String text);
}
interface Translator {
@UserMessage("Translate to {{lang}}:\n\n{{text}}")
String translate(@V("text") String text, @V("lang") String language);
}
public class SummariseAndTranslate {
private final Summariser summariser;
private final Translator translator;
public SummariseAndTranslate(ChatModel model) {
this.summariser = AiServices.create(Summariser.class, model);
this.translator = AiServices.create(Translator.class, model);
}
public String run(String document, String language) {
// The chain, as plain Java. Readable, testable, debuggable.
String summary = summariser.summarise(document);
return translator.translate(summary, language);
}
}The control flow is ordinary code. You can log between steps, add a cache, or short-circuit — none of which is possible when the sequence is hidden inside a framework abstraction.
A routing chain
Classify the input, then dispatch to a specialised handler. This is the most useful chain pattern in practice:
enum Category { BILLING, TECHNICAL, GENERAL }
interface Router {
@UserMessage("Classify this request as BILLING, TECHNICAL or GENERAL:\n{{it}}")
Category classify(String request);
}
public class RoutingAssistant {
private final Router router;
private final BillingAssistant billing;
private final TechnicalAssistant technical;
private final GeneralAssistant general;
public String handle(String request) {
// Cheap model classifies; specialised services (possibly on different
// models) handle. This is a chain and, importantly, not an agent —
// the routing is deterministic.
return switch (router.classify(request)) {
case BILLING -> billing.answer(request);
case TECHNICAL -> technical.answer(request);
case GENERAL -> general.answer(request);
};
}
}Adding deterministic steps
The strength of building chains in plain Java is that non-model steps slot in naturally:
public Report analyse(String document) {
// Step 1: model extracts entities.
Entities entities = extractor.extract(document);
// Step 2: deterministic enrichment from your database — no model needed.
List<EnrichedEntity> enriched = entities.list().stream()
.map(e -> database.lookup(e))
.toList();
// Step 3: model composes a report from the enriched data.
return writer.compose(enriched);
}Steps 1 and 3 are model calls; step 2 is a database lookup. Interleaving deterministic logic between model calls is where chains earn their keep — the reliable parts stay reliable.
Chain vs single call vs agent
| Situation | Use |
|---|---|
| One model call does the job | A single call |
| Fixed sequence of distinct stages | A chain |
| Steps depend on runtime results in unknowable ways | An agent |
The decision tree: if a single prompt suffices, do that. If you can write the steps down in advance, chain them. Only when you genuinely cannot predict the sequence should you reach for an agent, which trades predictability for flexibility.
Error handling across a chain
A chain fails at whichever step errors. Decide per step whether to fail the whole chain, retry, or degrade:
public String run(String document, String language) {
String summary;
try {
summary = summariser.summarise(document);
} catch (Exception e) {
log.warn("summarisation failed, using truncated original", e);
summary = truncate(document, 500); // degrade rather than fail
}
return translator.translate(summary, language);
}Next
- LangChain4j agents and tools — when the sequence cannot be fixed
- Agent architecture patterns — chains vs agents in depth
- LangChain4j retrievers and RAG
Frequently Asked Questions
What is a chain in LangChain4j?
Should I use a chain or a single prompt?
How is a chain different from an agent?
Can different steps in a chain use different models?
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 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 Introduction & ArchitectureA complete LangChain4j introduction for Java developers: core abstractions, the AiServices declarative style, memory, tools and retrieval — plus an honest comparison with Spring AI.
- 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.