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.

Su San Talks Tech
Su San Talks Tech
Su San Talks Tech
How to Build a Chat Service with Memory Using Spring AI 2.0

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-pro
Step 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-v8

Add 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-chat

Define 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.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaRAGRedisStreamingOpenAISpring AIChatClientChat Memory
Su San Talks Tech
Written by

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.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.