Spring AI EmbeddingModel Source Code Analysis
Embeddings are the silent engine behind modern AI systems. They transform words, images, and code into dense numerical vectors that capture semantic meaning, enabling search, clustering, and retrieval-augmented generation (RAG). Spring AI’s EmbeddingModel abstraction is not merely a convenience wrapper—it is an architectural boundary that decouples enterprise applications from the concrete embedding providers that generate those vectors. This article dissects the design of EmbeddingModel from a framework engineering perspective: the interface contract, the request/response model, the provider adapter layer, the integration with VectorStore, and the patterns that make it extensible, testable, and future-proof. We will walk through the source code structure, the decisions behind the abstraction, and the tradeoffs that every framework architect must face.
Understanding Embeddings
Before diving into the code, we need a shared conceptual foundation.
What Is an Embedding?
An embedding is a fixed-length list of floating-point numbers that represents a piece of content—text, image, audio—in a high-dimensional vector space. The key property is that semantically similar inputs produce vectors that are close together (cosine similarity, Euclidean distance). For example, the phrase “order status” and “where is my package” will have vectors that are closer than “order status” and “jump rope workout,” even though the literal word overlap is minimal.
How Embeddings Enable Semantic Search
Semantic search works by converting a user query into an embedding and then searching a vector database for stored document embeddings that are most similar. This retrieves documents that are conceptually related, not just keyword-matching. Enterprise knowledge bases, product catalogs, and internal wikis all benefit from this capability.
Why Embeddings Matter in RAG
Retrieval-Augmented Generation (RAG) grounds a language model’s responses in factual data retrieved from a knowledge base. The retrieval step relies on embedding-based similarity. A user’s question is embedded, relevant chunks are fetched from a vector store, and those chunks are injected into the prompt sent to the LLM. Without embeddings, RAG collapses.
The embedding provider (OpenAI, Azure, Bedrock, Ollama) generates the vectors; the embedding consumer (the RAG pipeline, the search service, the clustering job) uses those vectors. Spring AI’s EmbeddingModel exists precisely to make the consumer ignorant of which provider is at work.
Position of EmbeddingModel in Spring AI Architecture
The diagram reveals a clean separation: EmbeddingModel sits between the application components that need vectors and the various providers that produce them. It does not interact directly with ChatModel; instead, the QuestionAnswerAdvisor orchestrates the interaction: it uses EmbeddingModel to embed the user’s query, queries VectorStore with the resulting vector, and then delegates to ChatModel with the augmented prompt.
This design ensures that the retrieval logic remains provider-agnostic. You can swap the embedding model from OpenAI’s text-embedding-ada-002 to a self-hosted BGE model via Ollama, and the RAG pipeline requires no code changes—only a configuration update.
EmbeddingModel Interface Deep Dive
The interface is deliberately minimal. Here is the essence:
public interface EmbeddingModel extends Model<EmbeddingRequest, EmbeddingResponse> {
@Override
EmbeddingResponse call(EmbeddingRequest request);
default List<float[]> embed(List<String> texts) {
EmbeddingRequest request = new EmbeddingRequest(texts, EmbeddingOptions.EMPTY);
EmbeddingResponse response = call(request);
return response.getResults().stream()
.map(Embedding::getOutput)
.collect(Collectors.toList());
}
default float[] embed(String text) {
return embed(List.of(text)).get(0);
}
default int dimensions() {
return embed("test").length;
}
}
The single required method is call(EmbeddingRequest request). The convenience methods embed(String) and embed(List<String>) build a request, invoke call, and unwrap the response. This pattern—one core abstract method and pragmatic defaults—is borrowed from Spring Data’s repository interfaces and the broader *Template philosophy. It makes the contract easy to implement while giving implementors full control over the provider-specific invocation.
Why Spring AI Uses an Interface
The decision to define an interface rather than a class hierarchy or a static utility is rooted in two fundamental principles of enterprise framework design:
Dependency Inversion Principle
High-level modules (RAG pipeline, search services) should not depend on low-level modules (provider SDKs). Both should depend on abstractions. By declaring a dependency on EmbeddingModel, the application code becomes completely independent of the concrete provider. This enables:
- Vendor switching without touching business logic.
- Mocking in unit tests with a simple stub that returns deterministic vectors.
- Late binding at deployment time through Spring’s dependency injection.
Open/Closed Principle
The interface is open for extension (new providers can implement it) but closed for modification (the application code that consumes it never changes). Contrast this with a direct integration of the OpenAI SDK, where swapping to Anthropic would require finding every call site and rewriting the client logic.
Provider Independence and Testability
Consider a service that uses the OpenAI client directly:
// Tightly coupled, hard to test
var client = new OpenAiClient(apiKey);
var embedding = client.embeddings("text-embedding-ada-002", List.of("query"));
Testing this requires either mocking the HTTP layer (fragile) or hitting the real API (slow, costly, non-deterministic). With EmbeddingModel, the test becomes:
@MockBean
EmbeddingModel embeddingModel;
@Test
void testSearch() {
when(embeddingModel.embed("query")).thenReturn(new float[]{0.1f, 0.2f, ...});
// execute and verify
}
The abstraction pays for itself the first time you run a CI pipeline that doesn’t consume OpenAI credits.
EmbeddingRequest Design Analysis
public class EmbeddingRequest implements ModelRequest<List<String>> {
private final List<String> inputs;
private final EmbeddingOptions options;
public EmbeddingRequest(List<String> inputs, EmbeddingOptions options) { ... }
@Override
public List<String> getInstructions() { return inputs; }
public EmbeddingOptions getOptions() { return options; }
}
The first question a framework designer asks is: Why not just accept a String or List<String> directly in the call method? The answer lies in future extensibility and separation of concerns.
- Batch processing: Embedding APIs often support batching multiple texts in a single call for efficiency. The
List<String> inputsallows the provider adapter to choose the optimal batch size based on the underlying API’s limits. - Metadata and options:
EmbeddingOptionsis a provider-neutral container for parameters like dimensionality (OpenAI’sdimensions), encoding format (floatorbase64), and user identifier. Wrapping these alongside the inputs means the provider adapter can extract what it needs without polluting the input list. - Evolving without breaking: If a future provider requires a new type of input (e.g., multimodal images alongside text), the request can be extended with an additional field while maintaining backward compatibility via overloaded constructors. A raw
List<String>would force all callers to change.
This follows the same reasoning that led Spring MVC to introduce ServerHttpRequest instead of passing raw servlet parameters.
EmbeddingResponse Design Analysis
public class EmbeddingResponse implements ModelResponse<List<Embedding>> {
private final List<Embedding> embeddings;
private final EmbeddingResponseMetadata metadata;
public EmbeddingResponse(List<Embedding> embeddings, EmbeddingResponseMetadata metadata) { ... }
@Override
public List<Embedding> getResults() { return embeddings; }
public EmbeddingResponseMetadata getMetadata() { return metadata; }
}
public class Embedding {
private final List<Float> embedding;
private final int index;
// getters
}
The response wraps a list of Embedding objects, each containing the vector (List<Float> or float[]) and an index that maps back to the input ordering. The EmbeddingResponseMetadata carries provider-specific information: the model used, total token usage, and cost.
This design mirrors Spring’s JdbcTemplate RowMapper approach—the raw provider response is normalized into a consistent domain object that the rest of the application can consume without understanding vendor quirks. The metadata enables observability: an advisor can extract token counts and log or meter them, all without knowing the underlying provider’s response structure.
Provider Implementations
The concrete implementations translate the EmbeddingRequest into the provider’s native API call. The class hierarchy follows a template method pattern through an abstract base class:
public abstract class AbstractEmbeddingModel implements EmbeddingModel {
private final RetryTemplate retryTemplate;
@Override
public EmbeddingResponse call(EmbeddingRequest request) {
return retryTemplate.execute(ctx -> doEmbed(request));
}
protected abstract EmbeddingResponse doEmbed(EmbeddingRequest request);
}
AbstractEmbeddingModel centralizes retry logic (using Spring Retry) so that each provider adapter only needs to implement the HTTP call and response mapping.
Each doEmbed implementation:
- Converts
EmbeddingOptionsto the provider-specific parameters (e.g., OpenAI’sdimensionsfield). - Calls the provider’s REST API or SDK.
- Maps the response JSON into an
EmbeddingResponse.
Because the abstract class handles retries, the provider classes remain focused on the mapping, making them easy to write and review. Adding a new provider (e.g., Cohere) requires only a new subclass and a Spring Boot auto-configuration bean definition that registers it when the provider’s starter is on the classpath.
EmbeddingModel and VectorStore Collaboration
The true power of EmbeddingModel emerges when paired with VectorStore. The VectorStore interface does not know how to embed text; it only deals with already-embedded vectors or delegates embedding to an EmbeddingModel passed at the method level.
This interaction pattern highlights two critical design decisions:
- Separation of concerns: The vector store is responsible for storage and similarity computation; the embedding model is responsible for converting text into vectors. A
VectorStoreimplementation like PgVector never calls the embedding provider directly. - Flexibility: A single
VectorStorecan serve multiple use cases, each using a different embedding model if needed—for example, a multilingual embedding model for global search and a code-specific model for repository search. Passing theEmbeddingModelas a method parameter allows this without creating multiple vector store beans.
EmbeddingModel in a RAG Pipeline
In a full RAG pipeline, the embedding model participates in two distinct phases:
Indexing Phase
Documents are split into chunks, each chunk is embedded via EmbeddingModel, and the vector-content pair is stored in the VectorStore. The Document object in Spring AI carries the original text and metadata; the embedding vector is attached to it before storage.
Query Phase
The user query is embedded, and the VectorStore.similaritySearch returns the most similar Documents. These are concatenated into the prompt sent to the ChatModel.
The QuestionAnswerAdvisor encapsulates both phases transparently. It holds references to both EmbeddingModel and VectorStore, using them at query time. By keeping the embedding model and vector store as separate, pluggable components, Spring AI enables independently upgrading each layer—for instance, switching from PgVector to Weaviate without re-indexing if the embedding model remains the same, or re-indexing with a better embedding model without touching the retrieval logic.
Design Patterns Used
Spring AI’s EmbeddingModel subsystem is a textbook example of enterprise integration patterns.
| Pattern | Where Used | Purpose | Tradeoff |
|---|---|---|---|
| Strategy | Provider implementations are interchangeable strategies for the EmbeddingModel interface | Enables runtime selection of provider | Strategy must share a common interface; provider-specific features may not fit |
| Adapter | Each *EmbeddingModel adapts a third-party SDK to the Spring AI interface | Converts provider-specific API to a consistent model | Adds an additional layer; mapping logic may introduce bugs |
| Template Method | AbstractEmbeddingModel defines the skeleton (retry, call) while subclasses implement doEmbed | Enforces consistent error handling and retry semantics | Subclasses must adhere to the contract; cannot change the overall flow |
| Dependency Injection | EmbeddingModel is injected into consumers via the Spring context | Loose coupling, testability, central configuration | Requires Spring container; not usable in plain Java without bootstrapping |
| Simple Factory | Spring Boot auto-configuration creates the appropriate EmbeddingModel bean based on properties | Eliminates manual bean wiring; zero-code provider switching | Opaque startup failures if dependencies are missing |
These patterns combine to create a subsystem that is open for extension yet rigid enough to guarantee consistent behavior across providers.
Source Code Walkthrough: Auto-Configuration and Bean Lifecycle
When a Spring Boot application starts, the auto-configuration class EmbeddingModelAutoConfiguration reads the property spring.ai.embedding.backend (or infers it from the classpath) and instantiates the matching model.
Simplified structure:
@AutoConfiguration
@ConditionalOnClass(EmbeddingModel.class)
@EnableConfigurationProperties(EmbeddingProperties.class)
public class EmbeddingModelAutoConfiguration {
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(name = "spring.ai.openai.api-key")
public OpenAiEmbeddingModel openAiEmbeddingModel(OpenAiApi api, EmbeddingProperties props) {
return new OpenAiEmbeddingModel(api, props.getOpenai().getEmbedding().getOptions());
}
// similar beans for Azure, Ollama, etc.
}
The use of @ConditionalOnProperty and @ConditionalOnClass means that including the spring-ai-openai-spring-boot-starter dependency and providing an API key is sufficient to have a fully functional EmbeddingModel injected anywhere in the application. No manual bean definition is required.
The bean is typically scoped as singleton and is thread-safe because the underlying clients (OkHttp, WebClient) are safely shared. The framework avoids storing conversational state inside the model—embedding is a stateless operation.
Enterprise Benefits of the Design
Adopting EmbeddingModel as an architectural boundary yields concrete benefits beyond initial development:
- Vendor flexibility: The organization can start with OpenAI for convenience and later move to Azure OpenAI for data residency or to a self-hosted model for cost control, without rewriting the entire search and RAG layer.
- Maintainability: When a provider deprecates an API version, only the adapter class needs updating. The dozens of services that embed text remain untouched.
- Testability: Integration tests can use a lightweight Ollama container; unit tests can use a mock that returns a fixed vector, eliminating external dependencies.
- Cloud portability: The same application can run on AWS (Bedrock), GCP (Vertex AI), or Azure (Azure OpenAI) with configuration changes alone—crucial for multi-cloud strategies.
- Long-term architectural stability: The interface is unlikely to change because the concept of “text in, vector out” is universal. Extensions can add new methods without breaking existing code, preserving investment.
Design Tradeoffs
No abstraction is free. The EmbeddingModel design introduces several tradeoffs that architects must weigh.
Additional Abstraction Layer
Each provider adapter is an extra class that must be written, tested, and maintained. For teams already deeply invested in a single provider, this may feel like unnecessary indirection. However, the cost of adding an adapter (often under 200 lines of code) is dwarfed by the cost of vendor lock-in if the provider relationship changes.
Potential Feature Lag
The abstraction cannot expose every provider-specific feature on day one. For example, OpenAI’s dimensions parameter to reduce embedding size appeared in the API before Spring AI could elegantly map it into EmbeddingOptions. The framework must play catch-up, and early adopters may need to work around the abstraction temporarily.
Lowest Common Denominator Problem
The interface aims to unify across providers that may have fundamentally different capabilities. Some providers support embedding multimodal content (text + image); others do not. The abstraction must either ignore such features or awkwardly fit them into generic option maps, losing type safety. Spring AI currently focuses on text embeddings and leaves multimodal to a future iteration, which is a deliberate scope limitation.
Provider-Specific Features
When a provider offers a unique capability—say, a specialized embedding model fine-tuned for code—the abstraction cannot promote that model without adding a provider-specific configuration surface. Developers who need these features must cast to the concrete implementation, breaking the abstraction and creating a backdoor dependency. This is an acceptable compromise as long as the core 80% of use cases remain fully portable.
Comparison with Other Frameworks
| Aspect | Spring AI EmbeddingModel | LangChain4j | Direct OpenAI SDK | Custom Integration |
|---|---|---|---|---|
| Abstraction level | Interface + generic request/response | Similar interface (EmbeddingModel) with options | No abstraction; uses raw API objects | Fully bespoke |
| Provider switching | Configuration change | Configuration change | Full rewrite | Rewrite |
| Spring integration | Deep (Boot starters, Actuator, auto-config) | Requires manual bean creation or module integration | None | Manual |
| Retry & error handling | Built-in via AbstractEmbeddingModel | Can be added via interceptors | Manual | Manual |
| Batch optimization | Handled by adapter | Handled by adapter | Manual chunking | Manual |
| Testability | Mocking the interface | Mocking the interface | Mocking HTTP (WireMock) or real API | Varies |
| Learning curve | Low for Spring developers | Low for Java developers | Medium (SDK-specific) | High |
Both Spring AI and LangChain4j offer similar abstractions; the deciding factor is often ecosystem fit. A Spring Boot shop benefits from the autoconfiguration, property binding, and Actuator integration that Spring AI provides out of the box, reducing the boilerplate to nearly zero.
Lessons for Framework Designers
The EmbeddingModel subsystem offers several reusable design lessons:
- Stable abstractions over flexible implementations. The
EmbeddingModelinterface changes rarely; the implementations evolve rapidly. This protects consumer code from churn. - Provider independence through inversion. By injecting the model rather than hardcoding a provider, the framework enables late-binding decisions that are critical in enterprise environments.
- Future-proof interfaces with minimal surface. The single
call(EmbeddingRequest)method is maximally backward-compatible—new fields in the request do not break implementors. - API simplicity via sensible defaults. The
embed(String)convenience methods lower the entry barrier without sacrificing power; advanced users can usecalldirectly. - Separation of transport and semantics. The retry logic lives in the abstract base class; the HTTP wiring lives in the concrete adapter. This decomposition mirrors Spring’s
JdbcTemplatedesign and makes testing each layer independent.
Future Evolution
The current design is a foundation, not a final destination. Several extensions are likely:
- Multimodal embeddings: An
EmbeddingRequestthat can carry both text and image content, with providers like Vertex AI’s multimodal embedding model. - Hybrid retrieval: Combining dense (semantic) and sparse (keyword) vectors for better retrieval quality. The
EmbeddingModelmight provide both or a unified representation. - Sparse embeddings: Providers like Pinecone and Cohere offer learned sparse vectors that improve lexical matching. Spring AI could introduce a
SparseEmbeddingModelor extendEmbeddingModel. - GraphRAG integration: Embedding models that support entity extraction for graph-based retrieval could require additional response metadata, extending
EmbeddingResponse. - Enterprise vector intelligence: Auto-selecting the optimal embedding model based on the domain, cost, and latency requirements through a
EmbeddingModelrouter bean.
These evolutions will likely follow the same pattern: extend the model request/response, never break the core interface.
FAQ
1. How does Spring AI decide which embedding provider to use at runtime?
Auto-configuration detects the spring.ai.embedding.backend property or infers the provider from the presence of a starter (e.g., spring-ai-openai). If multiple providers are available, the property takes precedence; otherwise, a single candidate is selected.
2. Can I use two different embedding models in the same application?
Yes. Define multiple EmbeddingModel beans with qualifiers and inject them with @Qualifier. A common scenario is one model for English and another for multilingual content.
3. How does the abstract model handle rate limiting?
AbstractEmbeddingModel uses Spring Retry’s RetryTemplate with backoff policies. Provider-specific HTTP error codes (e.g., 429) are translated into RetryException by the adapter, triggering retry logic.
4. Why does EmbeddingModel extend Model<EmbeddingRequest, EmbeddingResponse>?
This generic superinterface (Model<REQ, RES>) is the base for all AI model abstractions in Spring AI, including ChatModel. It establishes a consistent call pattern and future hooks for observability.
5. Can I pass per-request options like dimensionality to the embedding call?
Yes. Build an EmbeddingOptions instance (provider-specific if needed) and pass it in the EmbeddingRequest. The adapter extracts supported options and ignores unknown ones.
6. How are batch sizes optimized?
The default embed(List<String>) method sends all texts in one request. Providers have limits; if exceeded, the adapter may chunk automatically. This behavior is provider-dependent and can be overridden by custom EmbeddingModel implementations.
7. Is the EmbeddingModel thread-safe?
Yes, all Spring-managed implementations are designed for concurrent access. The underlying HTTP clients and retry templates are thread-safe when properly configured.
8. How do I test a service that uses an EmbeddingModel without a real provider?
Create a mock bean that returns a constant vector. For more realistic tests, use the spring-ai-ollama starter with a local Ollama container and a lightweight embedding model during integration tests.
9. What happens if the provider changes its embedding dimension?
The consumer code should not hardcode vector dimensions. Use embeddingModel.dimensions() to dynamically adapt. If the dimension changes, vector store index might need rebuilding, but the application code remains unaffected.
10. Can I use Spring AI’s EmbeddingModel without Spring Boot?
Yes. Instantiate the provider adapter directly (e.g., new OpenAiEmbeddingModel(api, options)) and use it. However, you lose auto-configuration and must manage retry and error handling manually.
11. How does Spring AI handle embedding model churn (model deprecation)? The framework tracks provider API updates and releases new adapter versions. Because the interface is stable, you can update the Spring AI version independently of your application code and potentially swap the model name via configuration.
12. Does Spring AI support embedding models hosted on custom inference servers?
Yes. The Ollama integration handles any GGUF model, and the OpenAiApi can be pointed to any OpenAI-compatible endpoint (vLLM, TGI). You can also implement EmbeddingModel yourself for a proprietary server.
Conclusion
The EmbeddingModel interface in Spring AI is far more than a utility—it is the architectural keystone that separates enterprise application logic from the volatile world of embedding providers. By defining a minimal, stable contract and surrounding it with an adapter ecosystem, retry templates, and Spring Boot auto-configuration, the framework embodies the same design principles that made JdbcTemplate and RestTemplate indispensable. The design’s real value lies not in the relatively simple act of calling an embedding API, but in the clean, provider-independent seam it establishes. This seam enables organizations to build search, retrieval, and AI systems that can evolve with the technology landscape without requiring costly rewrites. For any Java architect building an AI-enabled platform on Spring, understanding and leveraging EmbeddingModel is not optional—it is a foundation for long-term architectural health.