Spring AI Complete Starter Project — Full Stack AI Application Template
This final post in the Spring AI series brings everything together in one production-ready starter project: a Spring Boot application with ChatClient, RAG, tool calling, streaming, security, caching, observability, and tests. Clone this structure to start any AI project with best practices already in place.
Project Structure
spring-ai-starter/
├── pom.xml
├── src/main/java/com/example/ai/
│ ├── AiApplication.java
│ ├── config/
│ │ ├── AiConfig.java ← ChatClient, VectorStore beans
│ │ └── SecurityConfig.java ← OAuth2 + Spring Security
│ ├── controller/
│ │ ├── ChatController.java ← REST + SSE streaming endpoint
│ │ └── RagController.java ← Document ingest + search
│ ├── service/
│ │ ├── ChatService.java ← Persistent chat with memory
│ │ ├── RagService.java ← Document ETL + retrieval
│ │ └── GuardrailService.java ← Input/output validation
│ ├── advisor/
│ │ ├── SafetyAdvisor.java ← Blocks harmful inputs
│ │ └── AuditAdvisor.java ← Logs all AI calls
│ ├── model/
│ │ ├── ChatSession.java ← JPA entity for conversation
│ │ └── AiAuditLog.java ← JPA entity for audit trail
│ └── tools/
│ └── UtilityTools.java ← @Tool methods
├── src/main/resources/
│ ├── application.properties
│ ├── application-dev.properties
│ └── prompts/
│ └── system-prompt.st
└── src/test/java/com/example/ai/
├── ChatServiceTest.java
└── RagServiceTest.java
pom.xml
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.0</version>
</parent>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>1.0.0</version>
<type>pom</type><scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency><groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-openai-spring-boot-starter</artifactId></dependency>
<dependency><groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-pgvector-store-spring-boot-starter</artifactId></dependency>
<dependency><groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-pdf-document-reader</artifactId></dependency>
<dependency><groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId></dependency>
<dependency><groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId></dependency>
<dependency><groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId></dependency>
<dependency><groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId></dependency>
<dependency><groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId></dependency>
<dependency><groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId></dependency>
</dependencies>
AiConfig.java
@Configuration
@EnableCaching
public class AiConfig {
@Bean
public ChatMemory chatMemory(ChatMemoryRepository memoryRepo) {
return new JpaChatMemory(memoryRepo); // persistent memory
}
@Bean
public ChatClient chatClient(ChatClient.Builder builder,
VectorStore vectorStore,
ChatMemory chatMemory,
SafetyAdvisor safetyAdvisor,
AuditAdvisor auditAdvisor) {
return builder
.defaultSystem(new ClassPathResource("prompts/system-prompt.st"))
.defaultAdvisors(
safetyAdvisor, // HIGHEST_PRECEDENCE
new MessageChatMemoryAdvisor(chatMemory),
new QuestionAnswerAdvisor(vectorStore),
new SimpleLoggerAdvisor(),
auditAdvisor // LOWEST_PRECEDENCE
)
.build();
}
}
ChatController.java
@RestController
@RequestMapping("/api/chat")
public class ChatController {
private final ChatService chatService;
@PostMapping
public ResponseEntity<ChatResponse> chat(
@RequestBody ChatRequest req,
Authentication auth) {
String reply = chatService.chat(auth.getName(), req.message());
return ResponseEntity.ok(new ChatResponse(reply));
}
@GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<ServerSentEvent<String>> stream(
@RequestParam String message,
Authentication auth) {
return chatService.stream(auth.getName(), message)
.map(token -> ServerSentEvent.<String>builder().data(token).build());
}
}
RagController.java
@RestController
@RequestMapping("/api/rag")
public class RagController {
private final RagService ragService;
@PostMapping("/ingest")
@PreAuthorize("hasRole('ADMIN')")
public Map<String, Integer> ingest(@RequestParam("file") MultipartFile file)
throws IOException {
int count = ragService.ingestDocument(file);
return Map.of("chunks", count);
}
@GetMapping("/search")
public List<SearchResult> search(@RequestParam String q,
@RequestParam(defaultValue = "5") int limit) {
return ragService.search(q, limit);
}
}
application.properties
spring.application.name=spring-ai-starter
# AI
spring.ai.openai.api-key=${OPENAI_API_KEY}
spring.ai.openai.chat.options.model=${AI_CHAT_MODEL:gpt-4o-mini}
spring.ai.openai.chat.options.temperature=0.7
spring.ai.openai.embedding.options.model=text-embedding-3-small
# Vector Store
spring.ai.vectorstore.pgvector.initialize-schema=true
spring.ai.vectorstore.pgvector.dimensions=1536
spring.ai.vectorstore.pgvector.distance-type=cosine_distance
spring.ai.vectorstore.pgvector.index-type=hnsw
# Database
spring.datasource.url=jdbc:postgresql://localhost:5432/aidb
spring.datasource.username=${DB_USER:postgres}
spring.datasource.password=${DB_PASSWORD:password}
spring.jpa.hibernate.ddl-auto=update
# Retry
spring.ai.retry.max-attempts=3
spring.ai.retry.backoff.initial-interval=1000ms
spring.ai.retry.backoff.multiplier=2
# Actuator
management.endpoints.web.exposure.include=health,info,metrics,prometheus
management.endpoint.health.show-details=when-authorized
Running the Starter Project
# 1. Start PostgreSQL with PGVector
docker run -d --name pgvector \
-e POSTGRES_DB=aidb -e POSTGRES_PASSWORD=password \
-p 5432:5432 pgvector/pgvector:pg16
# 2. Set environment variables
export OPENAI_API_KEY=sk-...
# 3. Run
./mvnw spring-boot:run
# 4. Test
curl -X POST http://localhost:8080/api/chat \
-H "Content-Type: application/json" \
-d '{"message": "What is Spring AI?"}'
# 5. Ingest a PDF (Admin only)
curl -X POST http://localhost:8080/api/rag/ingest \
-F "file=@document.pdf"
# 6. Stream response
curl "http://localhost:8080/api/chat/stream?message=What+is+RAG?"
Key Points
- This starter includes all the production concerns from the series: persistence, security, caching, observability, retry, and guardrails — in a working project structure
- The only required setup is a PostgreSQL database with pgvector and an OpenAI API key — everything else is already wired
- Swap the OpenAI starter for
spring-ai-anthropic-spring-boot-starterand update the model property to switch providers with no code changes - Environment variable references (
${OPENAI_API_KEY}) ensure the project never has secrets in committed files - The actuator Prometheus endpoint at
/actuator/prometheusexposes all Spring AI metrics (tokens, latency, cost) for Grafana dashboards out of the box
Comments