Prompts & Templates
|
This content was generated with the assistance of AI and should be verified against the official documentation of each tool and framework before being relied on in production. This section’s bibliography lists the reference material consulted while preparing these pages. |
Written against Spring AI 2.0.1, Spring Boot 4.1.x and Java 21 (verified 2026-09-29). This page adds prompt templates to the
running DocsAssistant service: its system prompt moves out of Java into a .st resource, the question is inserted
into a template as a parameter, and a JSON example in a prompt stops crashing the renderer. The prompt-writing craft
itself (clarity, examples, structure, chain-of-thought) is not repeated here; it lives in
Prompt Engineering Fundamentals and
Prompting Techniques, and this page shows which Spring AI class
carries each idea. The behaviours reported below were observed by running the snippets against a stub ChatModel and
the renderers directly; no model server was available.
What a Prompt is
A Prompt is the request object of the model API: an ordered list of Message objects and an optional ChatOptions.
Each message has a role, which Spring AI models as the MessageType enum with four values, and a class for each.
Role (MessageType) |
Class | Purpose |
|---|---|---|
|
|
Standing instructions: behaviour, tone, rules |
|
|
What the user asks or supplies. Can carry media for multimodal models |
|
|
A previous model reply. Also carries tool-call requests when the model wants a tool |
|
|
The result of a tool the model asked for |
Prompt has convenience accessors by role: getSystemMessage() (the first system message), getUserMessage() (the last
user message), getSystemMessages(), getUserMessages() and getLastUserOrToolResponseMessage(). A Prompt is
immutable in practice: methods such as augmentSystemMessage(…) and augmentUserMessage(…) return a new instance.
One trap is worth knowing. augmentSystemMessage(String) replaces the system text with the string it is given; to append
to it you use the function overload and read the current text, as the canary advisor in
Advisors does.
package com.example.docsassistant;
import java.util.List;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.SystemMessage;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.prompt.Prompt;
class PromptShapes {
// One message per role: a running conversation.
Prompt conversation() {
List<Message> messages = List.of(
new SystemMessage("You are DocsAssistant. Answer from the Irurueta docs only."),
new UserMessage("What does the vector-rag section cover?"),
new AssistantMessage("It covers embeddings, vector stores and RAG."),
new UserMessage("And which page shows the JDBC memory setup?"));
return new Prompt(messages);
}
// Few-shot prompting: earlier turns as user/assistant pairs teach the answer format.
Prompt fewShot(String pagePath) {
return new Prompt(List.of(
new SystemMessage("Classify a documentation page path into one component. Reply with the component only."),
new UserMessage("ai/langchain/tools.adoc"),
new AssistantMessage("langchain"),
new UserMessage("database/vector-rag/embeddings.adoc"),
new AssistantMessage("vector-rag"),
new UserMessage(pagePath)));
}
}
A Prompt can be passed to ChatModel.call(Prompt) or, through chatClient.prompt(prompt), into the fluent API. When
the conversation history comes from memory rather than from your code, an advisor adds the earlier messages for you; see
Chat Memory. Source:
Spring AI: Prompts.
PromptTemplate and SystemPromptTemplate
A PromptTemplate turns a text with placeholders and a map of values into text, a Message or a whole Prompt. Its
three levels of output are render(…) (a string), createMessage(…) (a user message) and create(…) (a
Prompt, optionally with ChatOptions). SystemPromptTemplate is the same class producing a SystemMessage, which is
the natural way to make a system prompt parameterised.
package com.example.docsassistant;
import java.util.List;
import java.util.Map;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.chat.prompt.PromptTemplate;
import org.springframework.ai.chat.prompt.SystemPromptTemplate;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Service;
@Service
class PromptTemplates {
@Value("classpath:/prompts/docs-system.st")
private Resource systemResource;
// A PromptTemplate renders variables into text and can create a Prompt directly.
Prompt questionPrompt(String component, String question) {
PromptTemplate template = new PromptTemplate("""
Component: {component}
Question: {question}
""");
return template.create(Map.of("component", component, "question", question));
}
// A SystemPromptTemplate built from a .st resource creates a system message.
Prompt withSystemResource(String tone, String question) {
Message system = new SystemPromptTemplate(this.systemResource).createMessage(Map.of("tone", tone));
return new Prompt(List.of(system, new UserMessage(question)));
}
}
Rendering behaves strictly by default. In tests, a template referring to {question} rendered with a map that lacked
that key threw an IllegalStateException saying that not all variables were replaced and naming [question]. That is a
feature: a forgotten variable fails loudly instead of sending the literal text {question} to the model. Values, on the
other hand, are inserted as plain text and are not interpreted again: a question containing {tone} or <x> reached the
prompt unchanged. This is why untrusted text belongs in a parameter, never concatenated into the template string: the
template stays under your control, and the user’s text is only ever data.
One API detail differs from older tutorials: the PromptTemplate constructor that took a variables map is no longer
public; use PromptTemplate.builder().template(…).variables(…).renderer(…).build() when you need that. Source:
Spring AI: Prompts.
Prompts as files
A long system prompt reads badly as a Java text block and cannot be changed without a rebuild. Spring AI accepts a
Spring Resource wherever it accepts a template string, so the prompt can live in src/main/resources/prompts/ with a
.st extension (the extension only signals the StringTemplate syntax; the content is plain text).
You are DocsAssistant, the helper of the Irurueta documentation site.
Reply in a {tone} tone and cite the page title when you can.
The SystemPromptTemplate(Resource) constructor above loaded this file and, given tone=friendly, produced "Reply in a
friendly tone and cite the page title when you can." The same resources work with ChatClient.Builder.defaultSystem(Resource),
defaultUser(Resource) and their per-request forms. Keeping prompts in files makes them reviewable in a pull request and
easy to diff, and it is the first step of the discipline described in
Managing Prompts. Source:
Spring AI: Prompts.
Templates in the fluent ChatClient
ChatClient uses the same machinery for its system and user text. Placeholders are filled with param(…) on the
system or user spec, and the messages are built when the call is made.
package com.example.docsassistant;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.stereotype.Service;
@Service
class TemplatedAnswers {
private final ChatClient chatClient;
TemplatedAnswers(ChatClient docsChatClient) {
this.chatClient = docsChatClient;
}
String answerAbout(String component, String question) {
return this.chatClient.prompt()
.system(s -> s.text("You answer questions about the {component} component.")
.param("component", component))
.user(u -> u.text("{question}").param("question", question))
.call()
.content();
}
}
Against a stub model this produced a system message "You answer questions about the vector-rag component." and a user
message with the question text. Defaults set with defaultSystem(…) and defaultUser(…) are templates too, so the
per-request .param(…) fills the placeholders declared once on the builder, as shown in
ChatModel & ChatClient. Source:
Spring AI: Chat Client API.
Delimiters: the brace problem
The default renderer, StTemplateRenderer, is built on the StringTemplate engine and uses { and } as delimiters.
That collides with JSON, which is exactly what prompts about structured output contain. A template with a literal JSON
example and a {topic} placeholder is not valid to the engine: rendering it threw an IllegalArgumentException
("The template string is not valid.") and the renderer logged the parser error.
There are three ways out, in order of preference.
-
Change the delimiters with
StTemplateRenderer.builder().startDelimiterToken('<').endDelimiterToken('>'). The prompt can then contain braces freely and placeholders are written<topic>. -
Keep the template free of braces by passing the JSON example as a parameter value, since values are not re-rendered.
-
Use
NoOpTemplateRenderer, which returns the text unchanged, when no substitution is wanted at all.
package com.example.docsassistant;
import java.util.Map;
import org.springframework.ai.chat.prompt.PromptTemplate;
import org.springframework.ai.template.st.StTemplateRenderer;
class JsonSafeTemplates {
String jsonSafe(String topic) {
PromptTemplate template = PromptTemplate.builder()
.renderer(StTemplateRenderer.builder().startDelimiterToken('<').endDelimiterToken('>').build())
.template("""
Reply with JSON like {"title": "...", "component": "..."} for a page about <topic>.
""")
.build();
return template.render(Map.of("topic", topic));
}
}
The rendered result for advisors was Reply with JSON like {"title": "…", "component": "…"} for a page about
advisors. The renderer also has a validation mode, ValidationMode.THROW by default, WARN and NONE, set with
validationMode(…); THROW is what produces the missing-variable failure described above, and lowering it trades a
loud error for silent literal placeholders, which is rarely worth it. The same renderer can be set for a whole client
with defaultTemplateRenderer(…) or for one request with templateRenderer(…). That renderer applies only to the
prompt text you give the client with system(…) and user(…); advisors that use their own templates, such as the
RAG advisors, keep theirs and have their own customisation points (see
RAG advisors). Source:
Spring AI: Prompts.
A custom TemplateRenderer
TemplateRenderer is a single-method interface, String apply(String template, Map<String, ? extends Object>
variables), so a different syntax is a small class. The renderer below replaces $name$ placeholders and leaves every
other character, including braces, alone, which makes it safe for prompts full of JSON. It fails on a missing value
instead of guessing.
$name$ renderer, used on a template and as a client defaultpackage com.example.docsassistant;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.prompt.PromptTemplate;
import org.springframework.ai.template.TemplateRenderer;
/** Replaces $name$ placeholders; anything else, including braces, is left untouched. */
class DollarTemplateRenderer implements TemplateRenderer {
private static final Pattern PLACEHOLDER = Pattern.compile("\\$(\\w+)\\$");
@Override
public String apply(String template, Map<String, ? extends Object> variables) {
Matcher matcher = PLACEHOLDER.matcher(template);
StringBuilder out = new StringBuilder();
while (matcher.find()) {
Object value = variables.get(matcher.group(1));
if (value == null) {
throw new IllegalStateException("No value for placeholder " + matcher.group(1));
}
matcher.appendReplacement(out, Matcher.quoteReplacement(value.toString()));
}
matcher.appendTail(out);
return out.toString();
}
}
class CustomRendererUsage {
String render() {
return PromptTemplate.builder()
.renderer(new DollarTemplateRenderer())
.template("Summarise the page $title$ as {\"summary\": \"...\"}.")
.build()
.render(Map.of("title", "Advisors"));
}
ChatClient clientWithRenderer(ChatModel chatModel) {
return ChatClient.builder(chatModel)
.defaultTemplateRenderer(new DollarTemplateRenderer())
.build();
}
}
render() returned Summarise the page Advisors as {"summary": "…"}. in a test. Because TemplateRenderer is a
BiFunction, it is easy to unit test in isolation. Source:
Spring AI: Prompts.
Prompt-engineering patterns in Spring AI
The Spring AI reference has a guide, Prompt
Engineering Patterns, that implements the techniques of the Prompt Engineering Guide with ChatClient. The table maps
each technique to the constructs on this and the neighbouring pages, so you can find the API without re-reading the
theory.
| Technique | In Spring AI | More |
|---|---|---|
Zero-shot |
|
|
One-shot and few-shot |
Example turns as |
|
Role and system prompts |
|
This page |
Structured answers |
|
|
Step-back, chain-of-thought and self-consistency |
Instructions in the prompt text and repeated calls; the framework adds nothing special |
|
Tool use (ReAct-style) |
|
|
Retrieval-augmented prompts |
A RAG advisor that adds retrieved passages to the prompt |
Prompts used in production should be versioned, reviewed and evaluated like code; a change to a system prompt is a behaviour change. Evaluation shows how to test them, and Security & Guardrails covers the case where a template is fed by untrusted text.
Where older material differs
The books and early blog posts predate several 2.0 details. PromptTemplate used to be created with a constructor taking a
map of variables, which is now the builder. Reading a reply as getContent() is getText(). Model
parameters that were set through .options property paths are flattened. The template syntax itself did not change:
{variable} with StringTemplate is still the default. When a snippet from an older source fails to compile, check these
first.