How to Build a Chat Service with Memory Using Spring AI 2.0
This article walks through integrating Spring AI 2.0 into a Spring Boot project, configuring model access, implementing synchronous and streaming chat endpoints, and adding Redis‑backed conversation memory to enable true multi‑turn interactions with large language models.
Spring AI Overview
Spring AI is the official Spring ecosystem extension for artificial‑intelligence workloads. It lets Java developers use AI models without dealing with low‑level machine‑learning code, similar to how they use Spring MVC or Spring Data.
Multi‑model, multi‑vendor support : one code base works with OpenAI, DeepSeek, Anthropic, Ollama, etc., covering chat, text‑to‑image, speech‑to‑text and more.
ChatClient streaming API : provides call() for synchronous calls and stream() for SSE‑style streaming.
Advisors API : wraps RAG, conversation memory, safety filters, etc., as reusable interceptor chains.
Chat Memory : built‑in in‑memory, Redis, JDBC and other back‑ends automatically manage multi‑turn context.
RAG (Retrieval‑Augmented Generation) : one‑line ETL pipeline vectorises documents for model consumption.
Tool Calling : annotate Java methods with @Tool so the model can invoke real APIs.
Project Integration
Step 1 – Add dependencies.
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>${spring-ai.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<!-- Spring AI OpenAI starter -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>
<!-- Spring Boot WebFlux (for reactive streaming) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>Because most models expose an OpenAI‑compatible API, the OpenAI starter is used. WebFlux is added to enable SSE streaming.
Step 2 – Add model configuration.
spring:
ai:
openai:
# DeepSeek service via OpenAI‑compatible endpoint
base-url: https://api.deepseek.com
api-key: ${DEEPSEEK_API_KEY}
chat:
model: deepseek-v4-proStep 3 – Define a ChatClient bean.
@Configuration
public class SpringAIConfig {
@Bean
public ChatClient chatClient(ChatClient.Builder builder) {
return builder.build();
}
}Implementing Chat Functionality
Create a ChatController with synchronous and streaming endpoints.
@RequiredArgsConstructor
@RestController
@RequestMapping("/chat")
@Tag(name = "ChatController", description = "Chat related APIs")
public class ChatController {
private final ChatClient chatClient;
private final ChatMemory chatMemory;
@Operation(summary = "Synchronous call")
@PostMapping("/call")
public String call(@RequestParam String question, @RequestParam String conversationId) {
// Retrieve history as context
List<Message> history = chatMemory.get(conversationId);
// Save user message
chatMemory.add(conversationId, List.of(new UserMessage(question)));
// Invoke model
String response = chatClient.prompt()
.messages(history)
.user(question)
.call()
.content();
// Save assistant reply
chatMemory.add(conversationId, List.of(new AssistantMessage(response)));
return response;
}
@Operation(summary = "Streaming call")
@PostMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<ChatEventDto> stream(@RequestParam String question, @RequestParam String conversationId) {
List<Message> history = chatMemory.get(conversationId);
chatMemory.add(conversationId, List.of(new UserMessage(question)));
StringBuilder full = new StringBuilder();
return chatClient.prompt()
.messages(history)
.user(question)
.stream()
.content()
.map(content -> {
full.append(content);
return ChatEventDto.builder()
.eventType(ChatEventType.DATA.getValue())
.eventData(content)
.build();
})
.concatWith(Flux.just(ChatEventDto.builder()
.eventType(ChatEventType.STOP.getValue())
.build()))
.doOnComplete(() -> {
if (!full.isEmpty()) {
chatMemory.add(conversationId, List.of(new AssistantMessage(full.toString())));
}
});
}
@Operation(summary = "Get conversation history")
@GetMapping("/history")
public CommonResult<List<Map<String, Object>>> history(@RequestParam String conversationId) {
var result = chatMemory.get(conversationId).stream()
.map(msg -> Map.of(
"role", msg.getMessageType().name(),
"content", msg.getText()))
.collect(Collectors.toList());
return CommonResult.success(result);
}
@Operation(summary = "Clear conversation history")
@PostMapping("/clearHistory")
public CommonResult<Void> clearHistory(@RequestParam String conversationId) {
chatMemory.clear(conversationId);
return CommonResult.success(null);
}
}The ChatEventDto used for SSE responses:
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(title = "ChatEventDto", description = "SSE event payload")
public class ChatEventDto {
@Schema(description = "Event type: 1001‑data, 1002‑stop, 1003‑parameter")
private Integer eventType;
@Schema(description = "Message content")
private Object eventData;
}Testing with Postman shows the synchronous /call endpoint returns the whole answer at once, while /stream streams JSON fragments incrementally.
Adding Conversation Memory
Spring AI supports several memory back‑ends; this guide uses Redis.
Run Redis Stack 7.4.0 (required for vector capabilities):
docker run --name redis-stack \
-p 6379:6379 \
-p 8001:8001 \
-d redis/redis-stack:7.4.0-v8Add the Redis chat‑memory starter:
<!-- Spring AI Redis Chat Memory Repository -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-chat-memory-repository-redis</artifactId>
</dependency>Configure Redis connection:
spring:
ai:
chat:
memory:
redis:
host: 192.168.3.101
port: 6379
time-to-live: 24h
key-prefix: spring-chatDefine beans for the repository and the memory implementation:
@Configuration
public class SpringAIConfig {
@Value("${spring.ai.chat.memory.redis.host}")
private String redisHost;
@Value("${spring.ai.chat.memory.redis.port}")
private int redisPort;
@Bean
public RedisChatMemoryRepository redisChatMemoryRepository() {
RedisClient redisClient = RedisClient.builder()
.hostAndPort(redisHost, redisPort)
.build();
return RedisChatMemoryRepository.builder()
.jedisClient(redisClient)
.build();
}
@Bean
public ChatMemory chatMemory(RedisChatMemoryRepository repository) {
return MessageWindowChatMemory.builder()
.chatMemoryRepository(repository)
.maxMessages(100)
.build();
}
}Inject ChatMemory into the controller (shown above) and use its add, get and clear methods, binding each conversation to a conversationId.
Functional Testing
A simple UI built with TDesign’s Chatbot component demonstrates the flow.
Ask a question, then ask “What did I ask before? Which turn am I on?” – the model correctly references the previous turn, confirming memory works.
Opening the Redis Stack UI shows the stored conversation entries.
The “history” endpoint returns the accumulated messages, and “clearHistory” removes them.
Conclusion
By adding the Spring AI starter, configuring model credentials, and injecting a ChatClient bean, developers can call large language models with just a few lines of code. Adding the Redis‑backed ChatMemory layer enables true multi‑turn conversations, solving the “always‑first‑turn” problem. Spring AI thus provides a familiar Spring‑style, low‑code path to embed AI capabilities into Java applications.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Su San Talks Tech
Su San, former staff at several leading tech companies, is a top creator on Juejin and a premium creator on CSDN, and runs the free coding practice site www.susan.net.cn.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
