Spring AI Project Structure
A well-organized project structure is the foundation of every maintainable, scalable Spring AI application. As your system evolves from a simple prototype to a production-grade platform handling retrievalβaugmented generation (RAG), autonomous agents, and multiple model providers, poor package organization leads to tangled dependencies, duplicated configuration, and fragile business logic. This guide provides a proven, production-ready layout that separates concerns, protects your core domain, and makes it easy to swap AI providers or upgrade framework components.
We will start with the essential layered architecture, explore a canonical directory structure, and then dive into each layer: configuration, AI components, RAG modules, multiβprovider support, prompt management, tool calling, agents, enterprise modularization, and testing. By the end, you will be able to design a Spring AI codebase that is clean, extensible, and aligned with Spring Boot conventions.
Typical Spring AI Application Architectureβ
A Spring AI application shares many architectural characteristics with a standard Spring Boot microservice, but it also introduces AIβspecific concerns such as model clients, advisors, vector stores, and prompt templates. We recommend a layered architecture that isolates these concerns while preserving a clear dependency flow.
Responsibilities:
- Presentation Layer β REST controllers, WebSocket handlers, or UI components. They accept user input and delegate to the application layer, never touching AI logic directly.
- Application Layer β Orchestration and business logic. Services here call
ChatClient, compose RAG pipelines, and coordinate domain objects. This layer knows what to do but not how the AI internals work. - AI Layer β Core AI building blocks:
ChatClient,ChatModel,PromptTemplate, advisors, memory, tool registrations, and streaming. This layer encapsulates the Spring AI framework specifics. - Domain Layer β Domain entities, value objects, and DTOs. These are plain Java objects with no framework dependencies, ensuring they remain portable and testable.
- Infrastructure Layer β Configuration, provider beans, external service clients, and crossβcutting concerns like logging and security. This is where autoβconfiguration and manual bean definitions reside.
- Data Layer β Vector stores (e.g.,
PgVectorStore), document repositories, and any other persistence mechanisms.
This layered approach mirrors classic Spring patterns and allows you to test each layer in isolation, swap AI providers without touching business logic, and extend functionality (e.g., adding agents) in a predictable manner.
Recommended Project Directory Structureβ
The following directory layout reflects the architecture above. It is suitable for a single-module Maven/Gradle project that can later be split into multiple modules when the codebase grows.
src/
βββ main/
βββ java/
β βββ com/example/ai/
β βββ config/
β β βββ AiConfig.java
β β βββ ProviderConfig.java
β βββ controller/
β β βββ ChatController.java
β βββ service/
β β βββ ChatService.java
β β βββ KnowledgeBaseService.java
β βββ ai/
β β βββ chat/
β β β βββ ChatClientProvider.java
β β βββ prompt/
β β β βββ PromptTemplateManager.java
β β β βββ templates/
β β βββ memory/
β β β βββ ChatMemoryConfig.java
β β βββ tools/
β β β βββ ToolDefinitions.java
β β β βββ WeatherTool.java
β β βββ advisors/
β β β βββ LoggingAdvisor.java
β β βββ providers/
β β βββ openai/
β β βββ ollama/
β β βββ azure/
β βββ rag/
β β βββ ingestion/
β β βββ chunking/
β β βββ embedding/
β β βββ retrieval/
β β βββ reranking/
β βββ vectorstore/
β β βββ VectorStoreProvider.java
β βββ agent/
β β βββ AgentService.java
β β βββ plans/
β βββ workflow/
β β βββ WorkflowEngine.java
β βββ model/
β β βββ ChatMessage.java
β β βββ DocumentChunk.java
β β βββ AgentState.java
β βββ dto/
β β βββ ChatRequest.java
β β βββ ChatResponse.java
β βββ exception/
β β βββ AiExceptionHandler.java
β βββ repository/
β β βββ DocumentRepository.java
β βββ util/
β βββ PromptUtils.java
βββ resources/
βββ application.yml
βββ application-dev.yml
βββ prompts/
β βββ system-qa.st
β βββ summarizer.st
βββ templates/
βββ documents/
Purpose of each directory:
- config/ β All Spring configuration classes (e.g.,
@Configuration,@Beandefinitions) that wire AI components together. - controller/ β REST endpoints that receive user requests and delegate to services.
- service/ β Business logic and orchestration. These classes should be frameworkβagnostic and depend on interfaces rather than concrete AI implementations.
- ai/ β The heart of your AI logic, divided by concern:
- chat/ β Chat client instantiation and helper methods.
- prompt/ β Prompt template management and loading from resource files.
- memory/ β Custom chat memory implementations or configuration.
- tools/ β
@Tool-annotated methods and tool registries. - advisors/ β Custom advisors for logging, security, or context enrichment.
- providers/ β Providerβspecific adapters or configuration, if you need fineβgrained control beyond autoβconfiguration.
- rag/ β Retrievalβaugmented generation pipeline components, organized by stage (ingestion, chunking, embedding, retrieval, reranking).
- vectorstore/ β Vector store abstractions and providerβspecific beans.
- agent/ β Agent implementations, planning logic, and execution loops.
- workflow/ β Workflow engine or state machine implementations (for advanced orchestration).
- model/ β Domain models and entities used across the application.
- dto/ β Data Transfer Objects for API requests/responses.
- exception/ β Global AI exception handling (e.g.,
@ControllerAdvice). - repository/ β Data access for relational or NoSQL databases (excluding vector stores, which live in
vectorstore/). - util/ β Static utility functions (prompt helpers, token estimators, etc.).
Resource directories:
- prompts/ β
.st(StringTemplate) files or plain text templates for system and user prompts. - templates/ β Thymeleaf or other view templates (if your application serves a UI).
- documents/ β Sample documents used for testing or seeding local vector stores.
This layout scales naturally. When you later decide to split the application into multiple Maven modules, you can lift entire packages (e.g., ai/, rag/) into their own modules with minimal refactoring.
Configuration Layerβ
Organize configuration classes to mirror the features they set up.
@Configuration
public class AiConfig {
@Bean
public ChatClient chatClient(ChatClient.Builder builder) {
return builder
.defaultAdvisors(new LoggingAdvisor())
.build();
}
}
If you support multiple providers, create separate configuration classes or use Spring profiles:
OpenAiConfig.javaOllamaConfig.javaAzureOpenAiConfig.java
Each class is annotated with @Profile("openai") and defines the appropriate ChatModel bean. Centralize prompt template loading in a PromptTemplateConfig that scans the prompts/ directory and exposes a PromptTemplateManager.
Environmentβspecific settings are managed via application-{profile}.yml. For example, application-dev.yml might use an Ollama local model, while application-prod.yml points to Azure OpenAI with higher timeouts.
AI Components Organizationβ
The ai/ package contains all Spring AIβspecific components, neatly separated by role.
ChatClientβ
Create a dedicated provider class that constructs and configures the ChatClient bean. This centralizes the default settings (temperature, model, advisors) and makes it easy to swap configurations per environment.
ChatModelβ
Wrap lowβlevel ChatModel access behind a simple service interface if you need to support providerβagnostic operations. For most use cases, ChatClient is sufficient.
PromptTemplateβ
Externalise prompt templates as .st files under resources/prompts/. Load them with @Value or a custom PromptTemplateManager. This decouples prompt engineering from code and enables versioning, A/B testing, and localization.
Advisorsβ
Place custom advisors in ai/advisors/. A logging advisor that records prompts and responses, or a security advisor that sanitizes inputs, are common examples. Advisors are then registered on the ChatClient builder.
Memoryβ
Chat memory implementations (e.g., inβmemory, JDBCβbacked) go into ai/memory/. The configuration class selects the appropriate ChatMemory bean based on the active profile.
Tool Callingβ
Tool definitions are kept in ai/tools/. Each tool is a separate class annotated with @Tool. A tool registry can be created to collect and expose them to the advisor chain.
Structured Outputβ
Classes that handle JSONβtoβobject conversion can reside in ai/chat/ or be extracted into a dedicated ai/structured/ package if the application relies heavily on typed responses.
Streamingβ
Streaming endpoints might require special Fluxβbased controllers. Keep the streamingβspecific logic close to the chat client configuration; the controller merely subscribes.
Organizing RAG Modulesβ
A RAG pipeline consists of several sequential stages. The rag/ package mirrors this flow:
rag/
βββ ingestion/
β βββ PdfIngestionService.java
βββ chunking/
β βββ TokenChunker.java
βββ embedding/
β βββ EmbeddingService.java
βββ retrieval/
β βββ VectorStoreRetriever.java
βββ reranking/
βββ CohereReranker.java
- ingestion/ β Document readers and preprocessing (PDF, HTML, plain text).
- chunking/ β Custom
DocumentTransformerimplementations for splitting text. - embedding/ β Thin wrapper around
EmbeddingModelthat handles batching and error handling. - retrieval/ β Logic that queries the vector store, applies metadata filters, and returns candidate documents.
- reranking/ β Integration with external reβranking models or crossβencoders.
Each stage is a separate Spring bean, making it easy to replace or mock during testing. The orchestration is typically done in a service class (e.g., KnowledgeBaseService) that calls these beans sequentially.
Managing Multiple AI Providersβ
Supporting multiple providers (OpenAI, Azure, Ollama, etc.) is a firstβclass requirement for many enterprises. The project structure should reflect this without creating duplication.
Strategy:
- Use Spring profiles to select the active provider.
- Define a
ChatModelbean per profile inai/providers/. - Inject
ChatModel(orChatClient) into your services without ever referencing a concrete implementation.
Example package for providerβspecific code:
ai/
βββ providers/
βββ openai/
β βββ OpenAiConfig.java
βββ azure/
β βββ AzureOpenAiConfig.java
βββ ollama/
βββ OllamaConfig.java
If you need to access providerβspecific features (e.g., custom headers), encapsulate that logic inside the provider package and expose it through a common interface. Never leak providerβspecific types into the service layer.
Prompt Managementβ
Prompt templates should be treated as external resources, not as inline strings. Place them under resources/prompts/ with descriptive names:
system-qa.stsystem-summarizer.stuser-document-qa.st
Load them via a PromptTemplateManager:
@Component
public class PromptTemplateManager {
private final Map<String, PromptTemplate> templates = new HashMap<>();
public PromptTemplateManager(ResourceLoader resourceLoader) {
// scan prompts/ folder and load .st files
}
public PromptTemplate get(String name) {
return templates.get(name);
}
}
Version your prompts by keeping them in the same repository as your code (Git). For advanced scenarios, store prompts in a database or a dedicated config server, but always provide a local fallback for development.
Tool Calling Organizationβ
Tools are regular Spring beans annotated with @Tool. Group them by domain in ai/tools/:
ai/tools/
βββ WeatherTool.java
βββ DatabaseLookupTool.java
βββ CalculatorTool.java
βββ ToolRegistryConfig.java
The ToolRegistryConfig scans the application context and collects all @Tool beans. This registry is then passed to the advisor that enables tool calling. By isolating tools in their own package, you make it easy to add, remove, or test them in isolation.
Each tool should validate its inputs and return structured results. Error handling (e.g., wrapping exceptions in a userβfriendly message) can be centralized through an ToolExceptionHandler.
AI Agent Organizationβ
Agents add complexity by looping, planning, and maintaining state. Organize them under agent/:
agent/
βββ AgentService.java
βββ plans/
β βββ PlanGenerator.java
β βββ PlanStep.java
βββ execution/
β βββ AgentExecutor.java
βββ memory/
βββ AgentMemoryStore.java
- AgentService β Public API for invoking an agent.
- plans/ β Logic that generates a plan (stepβbyβstep instructions) from a user goal.
- execution/ β Loop that iterates through plan steps, calls tools, and updates memory.
- memory/ β Persistence for agent state across turns (can reuse
ChatMemoryor a dedicated store).
Agents often interact with MCP (Model Context Protocol). MCP client/server implementations can live in a separate mcp/ package under agent/ or at the top level if shared by multiple agent implementations.
Enterprise Project Structureβ
For large, multiβteam Spring AI projects, a single module becomes unwieldy. Split the application into Maven modules that align with the architectural layers:
- ai-core β Core interfaces (
ChatClient,ChatModel,VectorStore,DocumentReader) and common domain objects. - ai-provider-openai β OpenAIβspecific adapter (implements core interfaces).
- ai-provider-azure β Azureβspecific adapter.
- ai-rag β RAG pipeline components (ingestion, chunking, retrieval).
- ai-agent β Agent framework, planning, execution.
- ai-api β REST controllers and DTOs.
- ai-web β Web application (Thymeleaf, React) if needed.
- ai-monitoring β Observability integrations (metrics, tracing).
Each module has its own pom.xml (or build.gradle) and depends on ai-core. The ai-api module wires everything together and depends on the provider and RAG modules through runtime scope, enabling easy provider switching.
This modular layout enforces strict dependency boundaries and allows different teams to work on different modules in parallel. It also simplifies testing and deployment because you can build and deploy only the modules that changed.
Testing Structureβ
Mirror the production package structure under src/test/java/. This makes it easy to locate tests and ensures coverage.
src/test/java/com/example/ai/
βββ ai/
β βββ chat/
β βββ tools/
β βββ advisors/
βββ service/
βββ controller/
βββ rag/
Testing best practices:
- Unit tests β Use JUnit 5 and Mockito to test individual AI components. Mock
ChatModelandVectorStoreto test advisor logic or tool execution without making real API calls. - Integration tests β Use
@SpringBootTestwith profiles that point to local Ollama or an embedded vector store (e.g.,SimpleVectorStore). Verify endβtoβend flows. - Mock AI providers β Create a test configuration that replaces the
ChatModelbean with aMockChatModelthat returns predefined responses. This isolates your tests from external services. - Test prompts β Store testβspecific prompts under
src/test/resources/prompts/to validate template processing. - Performance tests β Add Gatling or JMeter scripts in a separate
src/test/perf/directory to measure token usage and response latency.
Common Mistakesβ
- Mixing business logic with AI logic β Services that build prompts, call
ChatClient, and process results become hard to test. Delegate AIβspecific operations to dedicated AI components. - Hardcoded prompts β Strings scattered across controllers and services make prompt tuning a nightmare. Externalize all prompts.
- Poor package organization β A single
aipackage with 50 files obscures dependencies. Split by feature and layer from the start. - Tight provider coupling β Importing
OpenAiChatModeldirectly in a service destroys portability. Always depend on theChatModelinterface. - Duplicated prompt templates β Copyβpasting prompts across services leads to inconsistencies. Centralize them in
prompts/. - Missing configuration abstraction β Every component that needs an API key shouldnβt read it from
application.ymldirectly. Use a configuration class that exposes typed properties. - Lack of separation of concerns β A single
AIServicethat handles chat, RAG, and tools will quickly become unmaintainable. Break it into smaller, focused services.
Best Practicesβ
- Keep AI logic isolated in the
ai/package, away from business services. - Prefer interfaces (
ChatModel,VectorStore,PromptTemplate) over concrete implementations. This enables testing and provider switching. - Externalize prompts into resource files and load them with a dedicated manager.
- Use dependency injection extensively. Let Spring wire together the advisor chain, tools, and providers.
- Design for provider switching from day one. Even if you start with only OpenAI, hide it behind the abstraction.
- Organize reusable components (advisors, tools, prompt templates) in a way that they can be shared across multiple services or modules.
- Maintain clean package boundaries:
service/depends onai/, not the other way around.ai/depends on interfaces, not on concrete provider classes. - Follow Spring Boot conventions: use
@Configurationclasses,application.ymlfor configuration, and@Componentscanning.
What's Nextβ
With a solid project structure in place, you are ready to deepen your understanding of individual Spring AI components:
- Spring AI Architecture β Understand the frameworkβs internal design.
- ChatClient Guide β Master the fluent API for building requests.
- ChatModel & Providers β Learn how to configure and switch AI backends.
- Prompt Engineering β Design effective prompts with templates.
- Tool Calling β Let the model invoke your Java methods.
- RAG Overview β Dive into retrievalβaugmented generation.
- Enterprise AI Patterns β Production security, observability, and deployment.
Key Takeawaysβ
- A clean project structure separates presentation, application, AI, domain, infrastructure, and data layers.
- The
ai/package organizes all Spring AIβspecific code: chat clients, prompts, tools, advisors, memory. - RAG pipelines benefit from a dedicated
rag/package with subβpackages for each stage. - Support multiple AI providers by hiding them behind profiles and the
ChatModelinterface. - Externalize and version prompt templates; never embed them directly in code.
- For large projects, split into Maven modules that enforce architectural boundaries.
- Mirror the production structure in tests to keep them maintainable.
- Avoid common pitfalls like provider coupling, hardcoded prompts, and mixing concerns.