🔥 0
0
Lesson 3 of 8 25 min +200 XP

Prompt Templates & System Prompts

Your bare ChatClient from the last lesson works, but it has no personality and no rules. Ask it "What is the capital of France?" and it will happily answer — even though ShopKart's assistant should politely stay on topic. Ask it to recommend a product and it might invent a discount that does not exist. The fix is not more code; it is a better prompt. In this lesson you shape the assistant's behaviour with system prompts and reusable templates.

System prompt vs user prompt

Every chat request carries messages of different roles. Two matter most right now:

  • The system prompt sets the ground rules: who the assistant is, its tone, what it may and may not do. The shopper never sees it. It is sent with every request and steers all replies.
  • The user prompt is the shopper's actual message for this turn.

Think of the system prompt as the job description you hand a new support agent on day one, and the user prompt as each individual customer question.

Two roles in one request SYSTEM prompt "You are Kartik, the ShopKart shopping assistant. Prices in Rs. Only discuss ShopKart products. Never invent discounts." Set once, applies to every turn. Shopper never sees it. USER prompt "Do you have running shoes under 3000 for flat feet?" Changes every turn. The shopper's message.

A default persona for the assistant

The cleanest place to set the persona is on the ChatClient.Builder, as a defaultSystem prompt. Every call from that client then inherits it, so you never repeat yourself. Configure it in a @Configuration class and inject the ready-made ChatClient.

package com.shopkart.assistant;

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class AssistantConfig {

    private static final String SHOPKART_PERSONA = """
        You are Kartik, the friendly shopping assistant for ShopKart,
        an Indian online store.

        Rules you must always follow:
        - Speak in clear, warm, concise English. A little Hinglish is fine.
        - All prices are in Indian Rupees; always write amounts like Rs 2,499.
        - Only help with shopping on ShopKart: products, orders, delivery, returns.
        - If asked about anything unrelated (politics, coding, homework),
          politely decline and steer back to shopping.
        - Never invent products, prices, discounts, or delivery dates.
          If you are unsure, say so and offer to help another way.
        - Keep answers short unless the shopper asks for detail.
        """;

    @Bean
    public ChatClient shopkartChatClient(ChatClient.Builder builder) {
        return builder
                .defaultSystem(SHOPKART_PERSONA)
                .build();
    }
}

Now inject ChatClient directly (not the builder) wherever you need it:

@Service
public class AssistantService {

    private final ChatClient chatClient;

    public AssistantService(ChatClient shopkartChatClient) {
        this.chatClient = shopkartChatClient;
    }

    public String ask(String question) {
        return chatClient.prompt()
                .user(question)
                .call()
                .content();
    }
}

Ask the off-topic question now and the behaviour changes:

Shopper: What is the capital of France?
Kartik:  I'm here to help you shop on ShopKart! I can't help with general
         trivia, but I'd love to help you find a product, track an order,
         or check our return policy. What are you looking for today?

That single string of instructions is doing real work. Prompt writing is a skill: be specific, list rules as short bullet points, and state what NOT to do as clearly as what to do.

Guardrails live in the system prompt

Guardrails are the rules that keep the assistant safe and on-brand. A few that matter for e-commerce, and how to phrase them:

RiskGuardrail instruction
Inventing discounts"Never mention or promise discounts, coupons, or offers unless they are provided to you in the context."
Made-up delivery dates"Do not guess delivery dates. If you don't have the data, say you'll check."
Going off topic"Only discuss ShopKart shopping. Decline unrelated requests politely."
Rude or unsafe content"Stay respectful. Refuse requests for anything illegal, hateful, or unsafe."
Over-promising"You cannot change prices or cancel orders yourself; direct the shopper to the right action."

Guardrails in a prompt are necessary but not bulletproof — a determined user can still try to talk the model out of them. We add stronger, code-level defences (input validation, tool-permission checks) in the final lesson. Prompt-level guardrails are your first and cheapest line of defence.

Prompt templates with variables

Hard-coding whole prompts is fine for a fixed persona, but often you want to build a prompt from data. Say ShopKart wants a "smart product blurb" — given a product's fields, ask the model to write a punchy one-line description. You do not want to concatenate strings by hand (fragile, and a source of prompt-injection bugs). Use a template with named placeholders and fill them with param().

Spring AI's default template renderer uses {placeholder} syntax. You supply values through the user(Consumer) form:

public String writeBlurb(Product product) {
    return chatClient.prompt()
            .user(u -> u
                .text("""
                    Write a single catchy sentence (max 20 words) to sell
                    this ShopKart product. Do not mention price.

                    Name: {name}
                    Brand: {brand}
                    Category: {category}
                    Rating: {rating} out of 5
                    Key tags: {tags}
                    """)
                .param("name", product.name())
                .param("brand", product.brand())
                .param("category", product.category())
                .param("rating", product.rating())
                .param("tags", String.join(", ", product.tags())))
            .call()
            .content();
}

Each {placeholder} in the text is replaced by the matching param() value before the prompt is sent. The template stays readable and the data is injected safely by the framework.

The same mechanism works on the system prompt. Suppose you want the assistant's tone to vary by customer segment (a "PRIME" member gets warmer, more premium language). Leave a placeholder in the default system prompt and fill it per request:

@Bean
public ChatClient shopkartChatClient(ChatClient.Builder builder) {
    return builder
            .defaultSystem(s -> s.text("""
                You are Kartik, the ShopKart shopping assistant.
                Adapt your tone for a {segment} customer.
                All prices in Indian Rupees. Stay on shopping topics only.
                """))
            .build();
}
public String askAs(String question, String segment) {
    return chatClient.prompt()
            .system(s -> s.param("segment", segment))   // fill the placeholder
            .user(question)
            .call()
            .content();
}

Standalone PromptTemplate

When a prompt is reused in many places or assembled in stages, build it as a first-class PromptTemplate object and render it explicitly. This keeps template text out of your call sites.

import org.springframework.ai.chat.prompt.PromptTemplate;
import org.springframework.ai.chat.prompt.Prompt;
import java.util.Map;

public String compareTwoProducts(Product a, Product b) {
    PromptTemplate template = new PromptTemplate("""
        Compare these two ShopKart products for a shopper who wants the best
        value. Give a short verdict and one reason. Do not mention exact prices.

        Option A: {nameA} by {brandA}, rated {ratingA}/5
        Option B: {nameB} by {brandB}, rated {ratingB}/5
        """);

    Prompt prompt = template.create(Map.of(
            "nameA", a.name(), "brandA", a.brand(), "ratingA", a.rating(),
            "nameB", b.name(), "brandB", b.brand(), "ratingB", b.rating()));

    return chatClient.prompt(prompt)
            .call()
            .content();
}
template.create(map) returns a Prompt, which you hand straight to chatClient.prompt(prompt). Use inline .user(...).param(...) for one-off prompts, and a standalone PromptTemplate when the same template is shared across services or built dynamically.
On the job

Treat prompts like code: keep them in one place, version them, and review changes. A one-word edit to a system prompt can shift behaviour across every conversation, so put your important personas behind a config bean and test them. Never build a prompt by string-concatenating raw user input into instruction text — that is how prompt-injection attacks get in. Use param() placeholders so user data lands in a data slot, not an instruction slot.

Key takeaways

  • The system prompt defines who the assistant is and its rules; the user prompt is each shopper message.
  • Set a persona once with defaultSystem(...) on the builder so every call inherits it.
  • Guardrails (stay on topic, no invented discounts, no fake dates) belong in the system prompt as clear, specific bullet rules — a first line of defence, not a complete one.
  • Build data-driven prompts with {placeholder} templates filled by .param(...), on both user and system messages.
  • Use a standalone PromptTemplate and template.create(map) when a template is reused or assembled dynamically.
  • Never concatenate raw user input into instruction text; inject it through params.

Next up: Structured Output to Java POJOs — turning free-form replies into typed records you can use in code.

Talking to LLMs with ChatClient