jcardena.com Blog Designing adapters that outlive the vendors behind them
145 posts
EN ES

Designing adapters that outlive the vendors behind them

Software

Stop coupling your core logic to specific LLM or vector DB vendors. Learn to build a proper Adapter Pattern, or Anti-Corruption Layer, for AI systems.

Designing adapters that outlive the vendors behind them

The email arrived on a Tuesday morning. A foundational LLM provider we used for semantic search was deprecating their v1 embeddings API and changing their pricing model, effective in 60 days. This wasn't a panic, but it was a drain. It meant our product roadmap was now frozen for a sprint, dedicated to an expensive, unplanned migration. I’ve seen this happen enough times to know the root cause: we had allowed a vendor’s implementation detail to become an architectural pillar.

We often treat third-party services, especially in the fast-moving AI space, as permanent fixtures. We adopt their Python SDKs, structure our prompts to their specific formats, and write business logic that depends on their unique error codes. This is a direct path to fragility. The only way to build durable systems is to treat every external service as a volatile component, hidden behind a stable adapter that you control.

The Vendor Is an Implementation Detail

The fundamental mistake is letting a vendor’s worldview bleed into your own. When your code is full of `openai.Completion.create()` or logic to handle Anthropic-specific `RateLimitError` exceptions, you have coupled your system's fate to theirs. Your architecture is now partially defined by their product managers, their release cycles, and their venture funding. The goal is to write code that expresses your business logic, not your vendor's API.

The solution is a hard boundary. The Adapter pattern, or what Domain-Driven Design more aptly calls an Anti-Corruption Layer, is the tool for this. It's a dedicated module whose only job is to mediate between your system and an external one. Your application code speaks only to your adapter, using your own clean, internal interfaces and data models. The adapter, in turn, does the messy work of translating those requests into the specific format the vendor expects and translating the vendor’s response back into your internal format.

Core SystemYour CanonicalRequestAdapter TranslatesSpecific VendorAPI
The Adapter Pattern in Action

This isn't just about swapping vendors. It's about intellectual control. Your core domain logic—whether it's running a reasoning agent or a data quality pipeline—should be pristine, with no knowledge of who provides the embeddings or text generation.

Anatomy of a Modern AI Adapter

A good adapter for an AI service is more than a thin wrapper. It's a robust component that standardizes three things: the interface, the data model, and the failure modes.

First, the Internal Interface. This is the contract your application code uses. Instead of a method that mirrors a vendor, like `get_openai_embedding()`, it should reflect a business capability: `vector_service.create_embedding(text)`. This is stable, clear, and makes no assumption about the underlying model.

Second is the Translation Layer. This is where the vendor-specific mess lives. It takes your canonical inputs and transforms them. For an LLM adapter, it might map your generic `prompt` object into an OpenAI `messages` array or an Anthropic-style system prompt. This layer handles the specific authentication, formats the request body, and makes the network call. Large-scale open-source projects like LangChain are, at their core, massive collections of these adapters, which demonstrates the power of the pattern. Their list of LLM integrations is a perfect catalog of real-world adapters.

Finally, there's Policy and Failure Handling. No LLM API is 100% reliable or free of rate limits. The adapter is the perfect place to implement policies like automatic retries with exponential backoff or circuit breakers. It catches vendor-specific exceptions—`context_length_exceeded`, `rate_limit_error`—and translates them into a small, stable set of internal errors your application can actually handle, like `InvalidInputError` or `ProviderCapacityError`.

The Leaky Abstraction Counter-Argument

This pattern is not a silver bullet. An adapter is a deliberate trade-off: you sacrifice some short-term velocity and vendor-specific features for long-term resilience. The primary counter-argument, and it’s an important one, is the risk of creating a "leaky" or lowest-common-denominator abstraction.

If you build an adapter over two different vector databases, you might only be able to use the features they both share. You lose the ability to leverage a unique indexing strategy or a performance-critical feature from one of them. For truly foundational platforms—your primary cloud provider like AWS or your data warehouse like BigQuery—tightly coupling to its native capabilities is often the correct, pragmatic choice. Abstracting away S3 to make it look like Google Cloud Storage often results in a system that uses both poorly. As Gergely Orosz points out in his discussion on the pitfalls of multi-cloud, such an abstraction can be a "tremendous engineering effort that comes at a high cost." The key is to apply this pattern judiciously to the parts of your stack that are genuinely volatile, and the AI provider ecosystem is exactly that.

The Payoff: Agility in a Volatile Market

The real value of this approach is agility in a market defined by constant change. When a new, more capable, or 10x cheaper model is released, the challenge is contained. You "just" need to write a new adapter, not perform open-heart surgery on your entire codebase. You can A/B test models for quality and cost, routing 5% of traffic to the new provider by changing a single line of configuration.

This allows you to build sophisticated logic on top of your stable internal interface. You could, for instance, create a routing adapter that sends simple classification tasks to a cheap, local model while sending complex reasoning tasks to a state-of-the-art provider. This is only possible if your core logic isn't hard-coded to a single vendor's API. Architecting for this reality from day one is what separates systems that can evolve from those that become legacy the moment the next big model is released.

SOURCESUser InputsEvent StreamsBatch FilesINGESTION AND PROCESSINGDeterministicPipelineAgenticOrchestratorAdapter: LLMGatewayAdapter: Vector DBSTORAGE AND STATEVector StoreData WarehouseApplication DBSERVING LAYERSemantic SearchAPIAgent ToolsAnalyticsDashboards
Resilient Data and AI Architecture

What to Remember

  • Isolate Volatility: Treat external AI services—LLM providers, vector databases, transcription APIs—as volatile dependencies. Build a dedicated adapter for each business-critical one.
  • Define Your Canonical Models: Your internal systems should operate on your own data structures, like a clean `Prompt` or `DocumentChunk` object, not the vendor's. The adapter's job is to translate between your world and theirs.
  • The Adapter Owns the Mess: Consolidate all the vendor-specific complexity—authentication, data transformation, retry logic, and error handling—inside the adapter. Your core business logic should remain clean.
  • Apply the Pattern Wisely: This pattern is for managing volatile, swappable components. For foundational, stable platforms like your core cloud provider, embracing their native services is often the more pragmatic path.
JC
Juan Cardena
Enterprise Architect, Data & AI

Enterprise architect with 25 years across web, software, data, and AI. MIT CDAO ’25. Writing on agentic AI in production.