Spring AI Dynamic System Prompts — Personalize AI Behavior Per User
A system prompt defines the AI's persona, capabilities, and constraints for the entire conversation. Static system prompts treat all users the same. Dynamic system prompts read user preferences, roles, subscription level, and tenant data to give every user a personalized AI experience while keeping your application code clean.
Why Dynamic System Prompts Matter
Static system prompt:
"You are a helpful assistant."
Dynamic system prompt for Admin user:
"You are a Java expert assistant for Ravi Kumar (Admin).
You have access to sensitive system data.
Respond in English.
Current date: 2026-06-12."
Dynamic system prompt for Trial user:
"You are a helpful Java assistant.
You are in trial mode — do not provide full code solutions.
Instead, provide hints and guide the user.
Respond in Hindi."
Result: Same ChatClient code, completely different AI behavior
User Preferences Entity
@Entity
@Table(name = "user_preferences")
public class UserPreferences {
@Id
private String userId;
private String displayName;
private String preferredLanguage; // "English", "Hindi", "Spanish"
private String expertiseLevel; // "beginner", "intermediate", "expert"
private String subscriptionTier; // "trial", "pro", "enterprise"
private String timezone;
private boolean preferCodeExamples;
private boolean preferConciseAnswers;
@ElementCollection
private List<String> topics; // ["spring", "java", "microservices"]
}
System Prompt Builder Service
@Service
public class SystemPromptBuilder {
private final UserPreferencesRepository prefRepo;
public SystemPromptBuilder(UserPreferencesRepository prefRepo) {
this.prefRepo = prefRepo;
}
public String buildSystemPrompt(String userId) {
UserPreferences prefs = prefRepo.findById(userId)
.orElse(defaultPreferences());
return buildPrompt(prefs);
}
private String buildPrompt(UserPreferences prefs) {
StringBuilder prompt = new StringBuilder();
// Identity and purpose
prompt.append("You are a Java and Spring Boot expert assistant");
if (prefs.getDisplayName() != null) {
prompt.append(" helping ").append(prefs.getDisplayName());
}
prompt.append(".\n\n");
// Expertise adaptation
prompt.append(switch (prefs.getExpertiseLevel()) {
case "beginner" -> "Explain everything from first principles. Avoid jargon. Use analogies.\n";
case "intermediate" -> "Assume knowledge of Java basics. Explain Spring-specific concepts.\n";
case "expert" -> "Assume deep Java expertise. Be concise. Focus on advanced patterns.\n";
default -> "Adapt your explanations to the user's level.\n";
});
// Subscription-based feature restrictions
prompt.append(switch (prefs.getSubscriptionTier()) {
case "trial" -> "Trial mode: provide hints and partial examples only. Encourage upgrade for full solutions.\n";
case "pro" -> "Pro mode: provide complete, production-ready code examples.\n";
case "enterprise" -> "Enterprise mode: include security, scalability, and compliance considerations in all answers.\n";
default -> "";
});
// Response style
if (prefs.isPreferConciseAnswers()) {
prompt.append("Keep answers brief and to the point. No unnecessary explanation.\n");
}
if (prefs.isPreferCodeExamples()) {
prompt.append("Always include working code examples.\n");
}
// Language
if (prefs.getPreferredLanguage() != null &&
!prefs.getPreferredLanguage().equals("English")) {
prompt.append("Respond in ").append(prefs.getPreferredLanguage()).append(".\n");
}
// Topics context
if (prefs.getTopics() != null && !prefs.getTopics().isEmpty()) {
prompt.append("User's main interests: ")
.append(String.join(", ", prefs.getTopics()))
.append(". Prioritize these topics when relevant.\n");
}
// Current date for time-sensitive answers
prompt.append("\nCurrent date: ").append(LocalDate.now()).append(".");
return prompt.toString();
}
private UserPreferences defaultPreferences() {
UserPreferences defaults = new UserPreferences();
defaults.setExpertiseLevel("intermediate");
defaults.setSubscriptionTier("trial");
defaults.setPreferredLanguage("English");
return defaults;
}
}
Chat Service with Dynamic System Prompt
@Service
public class PersonalizedChatService {
private final ChatClient.Builder builder;
private final SystemPromptBuilder promptBuilder;
private final ChatMemory memory;
public PersonalizedChatService(ChatClient.Builder builder,
SystemPromptBuilder promptBuilder) {
this.builder = builder;
this.promptBuilder = promptBuilder;
this.memory = new InMemoryChatMemory();
}
public String chat(String userId, String userMessage) {
// Build dynamic system prompt for this specific user
String systemPrompt = promptBuilder.buildSystemPrompt(userId);
// Create a ChatClient with user-specific system prompt
// Note: system() overrides defaultSystem() at call time
return builder.build()
.prompt()
.system(systemPrompt) // dynamic per-user system prompt
.user(userMessage)
.advisors(new MessageChatMemoryAdvisor(memory),
a -> a.param(
MessageChatMemoryAdvisor.CHAT_MEMORY_CONVERSATION_ID_KEY,
userId))
.call()
.content();
}
}
Template-Based System Prompts
// src/main/resources/prompts/system-prompt.st
You are a Java assistant helping {userName}.
Expertise level: {level}
Subscription: {tier}
{#if tier == "enterprise"}
Include enterprise patterns (security, observability, scalability) in all answers.
{/if}
{#if preferConcise}
Be concise. Skip preamble. Get to the point.
{/if}
Respond in {language}.
Current date: {date}.
@Service
public class TemplatePromptBuilder {
public String buildFromTemplate(UserPreferences prefs) {
PromptTemplate template = new PromptTemplate(
new ClassPathResource("prompts/system-prompt.st"));
return template.render(Map.of(
"userName", prefs.getDisplayName(),
"level", prefs.getExpertiseLevel(),
"tier", prefs.getSubscriptionTier(),
"preferConcise", prefs.isPreferConciseAnswers(),
"language", prefs.getPreferredLanguage(),
"date", LocalDate.now().toString()
));
}
}
Multi-Tenant System Prompts
@Service
public class MultiTenantChatService {
private final ChatClient.Builder builder;
private final TenantConfigRepository tenantRepo;
public String chat(String tenantId, String userId, String message) {
TenantConfig tenant = tenantRepo.findById(tenantId)
.orElseThrow(() -> new TenantNotFoundException(tenantId));
// Build tenant-specific system prompt
String systemPrompt = """
You are %s, the AI assistant for %s.
Company domain: %s
Restricted topics (never discuss): %s
Company policies: %s
""".formatted(
tenant.getBotName(),
tenant.getCompanyName(),
tenant.getDomain(),
String.join(", ", tenant.getRestrictedTopics()),
tenant.getPoliciesSummary()
);
return builder.build()
.prompt()
.system(systemPrompt)
.user(message)
.call()
.content();
}
}
Output
// Beginner user asking "What is a Spring Bean?"
System prompt generated:
"You are a Java and Spring Boot expert assistant helping Alice.
Explain everything from first principles. Avoid jargon. Use analogies.
Trial mode: provide hints and partial examples only.
Respond in English.
Current date: 2026-06-12."
AI response:
"Think of a Spring Bean like an employee at a company.
The company (Spring container) is responsible for hiring (creating),
managing, and firing (destroying) employees (beans).
You don't hire them directly — you just request one when needed..."
// Expert user with enterprise subscription asking the same question:
System prompt: "...Assume deep Java expertise. Be concise..."
AI response:
"Spring beans are objects managed by the IoC container.
Lifecycle: instantiation → dependency injection → @PostConstruct → use → @PreDestroy.
Scopes: singleton (default), prototype, request, session, websocket..."
Key Points
- Call
.system(dynamicPrompt)at request time rather than.defaultSystem()at build time — it overrides the default and enables per-request personalization - Keep system prompts under 500 tokens — every token in the system prompt is sent with every message, multiplying cost at scale
- Cache system prompts per user (
@CacheableonbuildSystemPrompt()) — they change infrequently but are called on every message - Store prompt templates as external
.stor.txtfiles rather than Java string literals — product managers can tune them without code changes - Log the system prompt hash (not the full content) in metrics to detect when user-specific prompts change unexpectedly
Comments