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.

Table 1. Message roles and their Spring AI classes
Role (MessageType) Class Purpose

SYSTEM

SystemMessage

Standing instructions: behaviour, tone, rules

USER

UserMessage

What the user asks or supplies. Can carry media for multimodal models

ASSISTANT

AssistantMessage

A previous model reply. Also carries tool-call requests when the model wants a tool

TOOL

ToolResponseMessage

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.

PromptShapes.java: a conversation and a few-shot prompt built from messages
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.

PromptTemplates.java: templates that render to text, a message or a prompt
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).

src/main/resources/prompts/docs-system.st
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.

TemplatedAnswers.java: templates directly on the ChatClient
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.

JsonSafeTemplates.java: angle-bracket delimiters so JSON braces are left alone
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.

CustomRenderer.java: a $name$ renderer, used on a template and as a client default
package 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.

Table 2. Prompt techniques and where they live in Spring AI
Technique In Spring AI More

Zero-shot

chatClient.prompt("…​") with a low temperature set through options

Chat Options

One-shot and few-shot

Example turns as UserMessage and AssistantMessage pairs in a Prompt (see fewShot above), or examples inside the system text

Prompting Techniques

Role and system prompts

defaultSystem(…​), SystemPromptTemplate, .st files

This page

Structured answers

.entity(Type.class), which appends the JSON schema to the prompt

Structured Output

Step-back, chain-of-thought and self-consistency

Instructions in the prompt text and repeated calls; the framework adds nothing special

Reasoning Models

Tool use (ReAct-style)

@Tool methods and the tool-calling loop

Tool Calling

Retrieval-augmented prompts

A RAG advisor that adds retrieved passages to the prompt

RAG advisors

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.