Integration Patterns: Wrap, Don't Replace
When you adopt external code, your calling code should never touch the external API directly. The wrapper pattern gives you swappability, observability, retry logic, and a clean exit path — without rewriting your application.
You adopted an image generation API. It passed sec-scan. It filled a genuine capability gap. The decision framework gave it a green light.
Now comes the integration. And this is where most engineers make the mistake that turns a good adoption decision into a permanent liability.
They call the API directly from their application code.
Why Wrappers Are Non-Negotiable
Without a wrapper, your application code is coupled to the provider's API contract. Their response format. Their error codes. Their authentication pattern. Their rate limiting behavior. When any of these change — and they will — you are rewriting application code, not integration code.
The wrapper creates a boundary you own between your logic and their service. That boundary gives you four things:
Swappability. When the provider retires a model, changes pricing, or goes down permanently, you swap the implementation inside the wrapper. Your calling code does not change. Not one line. The wrapper's interface stays the same; only the internals change.
Observability. The wrapper logs every call. Request parameters, response time, status code, error messages. You cannot debug what you cannot see. When image generation starts failing silently, the wrapper's logs tell you exactly when it started and what the provider returned.
Control. Retry logic. Rate limiting. Caching. Circuit breakers. Timeout enforcement. None of these belong in your application code. All of them belong in the wrapper. The wrapper is where you impose your operational rules on someone else's service.
Exit path. When the 90-day review determines that this provider no longer earns its place, you rip out the wrapper's internals and replace them. Your application code — every file that calls the wrapper — remains untouched. The exit cost drops from "rewrite every integration point" to "rewrite one file."
The Wrapper Interface Pattern
The wrapper exposes a stable interface that your code depends on. The implementation behind that interface can change without affecting callers.
# your_project/wrappers/image_gen.py
class ImageGenerator:
"""Wrapper around external image generation providers.
Calling code imports THIS — never the provider SDK directly."""
def __init__(self, providers: list[ImageProvider]):
self.providers = providers
def generate(self, prompt: str, width: int, height: int) -> ImageResult:
"""Generate an image. Tries each provider in order.
Returns ImageResult with path, provider used, and latency."""
for provider in self.providers:
try:
result = provider.generate(prompt, width, height)
log_success(provider.name, result.latency)
return result
except ProviderError as e:
log_failure(provider.name, e)
continue
raise AllProvidersFailedError(prompt)
Your application code calls image_gen.generate(). It does not know or care whether the image came from Gemini, Leonardo AI, or OpenAI. It never imports openai or leonardo_sdk. It never handles provider-specific error codes. It calls the wrapper and gets an image or an error. That is the contract.
The Fallback Chain Pattern
The wrapper is where the fallback chain lives. Not in your application code. Not in a separate orchestration layer. In the wrapper.
Our image generation wrapper implements a three-provider chain:
- Gemini (mcp-image) — primary. Fastest, lowest cost on the free tier.
- Leonardo AI — fallback one. Phoenix 1.0 model, cinematic quality, rate-limited.
- OpenAI (gpt-image-1) — fallback two. Highest cost, highest reliability.
If Gemini returns an error, the wrapper immediately tries Leonardo. If Leonardo fails, it tries OpenAI. No retries on the same provider. No exponential backoff. Fail fast, route fast.
The same pattern applies to LLM routing:
- Flash — simple gather and classification tasks. Near-zero cost.
- Pro — moderate complexity. Still economical.
- Sonnet — complex reasoning that lower tiers cannot handle.
The calling code does not know the chain exists. It calls generate() and gets a result. The chain is an implementation detail of the wrapper — invisible to everything outside it.
What the Wrapper Controls
Beyond swappability and fallback routing, the wrapper is where you enforce operational discipline on external services:
Rate limiting. The provider allows 60 requests per minute. Your wrapper enforces 50 to stay under the limit with margin. The rate limit lives in the wrapper, not scattered across calling code.
Timeout enforcement. The provider's API sometimes hangs for 30 seconds. Your wrapper enforces a 10-second timeout and routes to the next provider. Your application code never waits 30 seconds.
Caching. If you generated an image for the same prompt in the last 24 hours, serve the cached version. The wrapper checks the cache before calling any provider. Zero API calls for repeated requests.
Circuit breaker. If a provider has failed five times in the last ten minutes, stop calling it. The circuit is open. Route directly to the next provider until the circuit resets. This prevents burning rate limit quota and latency on a provider that is clearly down.
# Circuit breaker inside the wrapper
if provider.failure_count > 5 and provider.last_failure < now() - timedelta(minutes=10):
log_circuit_open(provider.name)
continue # skip this provider entirely
The wrapper pattern is simple. A function that calls another function. The difficulty is the discipline to never bypass it — to never let a shortcut turn into a direct dependency that you discover during an incident at 3 AM.
Real-World Wrapper: Security Vetting Third-Party Processors
The wrapper pattern extends beyond APIs to any external service that processes your data. When evaluating third-party processors for Architect of War operations, the wrapper includes a vetting layer:
- Jurisdiction check. Where is the processor incorporated? Where is data stored?
- License verification. Is the service properly licensed for the data it handles?
- Custodial window. How long does the processor hold your data? Can you enforce deletion?
These checks are part of the wrapper's initialization — not afterthoughts. The wrapper refuses to send data to a processor that has not been vetted.
Lesson 147 Drill
Pick one external dependency in your current codebase that is called directly from application code — no wrapper, no abstraction layer. It might be a direct import openai in a route handler, or a raw HTTP call to a third-party API in a service function.
Build the wrapper:
- Define the interface your application code should depend on
- Move the provider-specific code into the wrapper's internals
- Update all calling code to use the wrapper instead of the direct dependency
- Add logging — every call, every response time, every error
- Identify at least one fallback provider and stub it in the chain
The exercise takes one to two hours. The payoff is permanent: that dependency can now be swapped, monitored, and controlled without touching your application code.
Bottom Line
When you adopt an external tool, you are not adding it to your stack. You are adding a wrapper to your stack that happens to use that tool today. The tool is replaceable. The wrapper is permanent.
Without the wrapper, adoption is a one-way door. With it, every adoption is a two-way door — you can walk back through it any time the dependency stops earning its place.