August 3, 2026
The 15 Minutes That Saved Me From a Vendor-Coupled Service
The situation
I was building a passport-scanning feature: upload an image or PDF, run it through a cloud OCR provider, parse the extracted text into structured fields. The first draft did exactly what most first drafts do. The service that orchestrated the scan called the OCR provider's SDK directly, right next to the parsing and validation logic.
It worked. But two things nagged at me almost immediately:
- Unit testing the parsing logic meant either hitting the real OCR API in tests or mocking a cloud SDK's client, request builder, and response shape, none of which had anything to do with what I was actually testing.
- If the OCR provider ever changed (cost, accuracy, contract terms), every place that imported the SDK's types would need to change too.
The principle
The Dependency Inversion Principle says high-level modules shouldn't depend on low-level modules; both should depend on an abstraction. In practice, at a vendor boundary, that means the code that decides what to do with a result shouldn't know which vendor produced it.
// Before: service depends directly on the vendor SDK
class DocumentService {
async scan(buffer: Buffer) {
const client = new VendorOcrClient(/* vendor-specific config */);
const response = await client.detectText({ Document: { Bytes: buffer } });
return response.Blocks.filter(b => b.BlockType === 'LINE').map(b => b.Text);
// parsing logic follows, now entangled with vendor response shape
}
}
// After: service depends on an abstraction, not a vendor
abstract class OcrEngine {
abstract extractText(buffer: Buffer): Promise<string[]>;
}
class VendorOcrEngine extends OcrEngine {
async extractText(buffer: Buffer): Promise<string[]> {
// all vendor-specific code lives here, and only here
}
}
class DocumentService {
constructor(private readonly ocrEngine: OcrEngine) {}
async scan(buffer: Buffer) {
const lines = await this.ocrEngine.extractText(buffer);
// parsing logic only ever sees string[], no vendor types in sight
}
}The refactor was small: one abstract class with a single method, one concrete implementation, and a change to how the service receives its dependency (constructor injection instead of direct instantiation). Nothing about the parsing or business logic changed at all.
How I applied it
Once the abstraction existed, the vendor-specific complexity (handling multi-page PDFs by splitting them into individual pages, auto-orienting images based on EXIF data before sending them to the OCR call) moved entirely inside the one implementation class. The orchestrating service never needed to know any of that was happening. It just called extractText and got lines of text back.
The broader takeaway
Dependency Inversion isn't an abstract OOP rule to satisfy for its own sake. It's a cost/benefit call about where you want change to be expensive. Vendor and infrastructure boundaries are exactly the place where future change is likely (pricing, reliability, feature gaps), so that's where the abstraction earns its keep. Drawing the line before the code grows around the vendor costs minutes. Drawing it after costs a much bigger refactor, usually right when you're under pressure to swap providers quickly.