A coworker recently asked about writing a wrapper around a third-party vendor and their feature set. The concern was how much abstraction to add around the vendor API - wondering if they should make the wrapper a simple pass-through to the vendor, or if they should try and create a more complex abstraction that happened to use the vendor's API.
The ultimate goal with this conversation was to determine if the client should be worried about vendor lock-in. If they abstract the vendor-specific code into something more generalized, it should be more resilient to vendor changes, right? While there is some truth in this, it can be a dangerous path.
If we're building something with the intent of quickly and easily dropping one provider in favor of another, there's a very real danger of writing an abstraction that only does the following:
- Creates an unfortunately complex abstraction
- Makes our code very difficult to understand, and
- Codes to the "lowest common denominator" between possible vendors
The first two are problems that we, as engineers, generally love to solve, and can often do very well — it's that last one where we find the real danger.
Coding to the lowest common denominator of features that vendors provide is going to ensure you have a generic, difficult-to-use, and feature-missing (not feature-rich) API that doesn't provide the level of value that your chosen vendor is capable of.
Coding to the Lowest Common Denominator?
If there are multiple vendors that can provide this service, there is a strong likelihood that each vendor offers something unique. Otherwise, there probably wouldn’t be so many vendors, and their expertise would likely be a commodity, open-source, or even built into a programming language.
With that in mind, consider writing a generic abstraction around the feature you need and doing so in a way that makes it easy to drop one vendor for another. It's a tempting thought and is something I have tried to do many times in the past.
Let's say you have two vendors you're choosing from, though. There may be some similarities in the solutions, and there may be some very different solutions, different features, and specializations that make one vendor stand out above the other for some scenarios.
Vendor A:
- OCR
- Hide PII (personally identifiable information, like name and birthdate, phone, etc)
- AI to hide irrelevant data
Vendor B:
- OCR
- Provide custom categorization and ML to learn about them
- Correlate multiple digitizations across many users to better understand the images and categories using AI
If I try to write something so abstract that I can drop a single vendor in favor of another because I only coded to the lowest common denominator, what am I left with? Just basic OCR.
In that situation, why am I bothering to use a vendor for that when OCR libraries are freely available? The value that both of these vendors provide isn't the commodified OCR capability. It's the additional feature set and expertise around the OCR.
Coding the vendor into your app, directly?
The other side of this coin is to code something so specific to a vendor that it ends up "infecting" the entire codebase with that vendor's terminology, toolset, libraries, etc. This can effectively make it impossible (or at least impractical) to change vendors.
If the vendor ends up not providing the services that are needed, if the vendor ends up shutting down, or for any number of other reasons, this is a dangerous path. Having code that is tied to a specific vendor, throughout the system, means that the vendor is locked in to the system. It can take months or even years to correct this.
What, then, is the right way to approach an abstraction or other use of a vendor?
Coding to your app's current needs
The trick, and where the real value lies in what that we provide as consultants, is to find the appropriate balance between vendor lock-in and the lowest common denominator.
My goal as a developer is to write code that targets the required feature set and capabilities of my system, while pushing the vendor specifics into the implementation in a way that allows me to still take advantage of the vendor's unique features and capabilities.
Instead of throwing out all caution and running with vendor lock-in as the norm, though, it's worth taking time to classify the vendor specific features as features your system provides. Keep the vendor specifics consolidated in and hidden behind your API, but keep them close to the surface of the API, so you don't end up with a monstrosity of complex code.
When it comes time to consider changing providers or tools, I at least have an API level abstraction to compare against. There's a very strong chance that I'll end up losing some features and deleting that code — but I will likely gain some features and add new code, too.
I'm not trying to find the ultimate, perfect abstraction that makes it easy to drop a vendor. But I'm also not trying to make my code work with just any vendor. There will be specifics for a vendor that I want to code against. At the same time, there should be some level of an API layer between the vendor and the rest of the system.
Fellow Test Doubler, Tom Nightingale, said it this way in that same conversation:
I try to think about the interface that our application requires to use the behavior provided by the 3rd-party.
If the interface is built in a behavior-centric way, it can be adapted to a new vendor in the future and will remain in the codebase as long as the feature using it exists.
This approach can also help avoid completely encapsulating the 3rd-party library and causing the "lowest common denominator" issue. If your application needs another feature offered by the 3rd-party then you're free to use that independently of what you have built to-date.
Good, Not 'Best,' Practices
There is no perfect solution here, because there are no truly "best" practices for all situations. And there's no "right" or "wrong" way to find the needed balance when bringing a new vendor or tool into your code, without understanding all of the details and complexities in an application, what it needs to do, and why.
But if I look at vendor-based solutions from this perspective, the conversation shifts from "what abstraction can I write to make this easier for switching?" to "what features does this system need from this provider, and potentially other providers being considered?"
In the end, it becomes a product conversation before it's a technical one. It also means the ability to change vendors becomes something to prioritize and migrate toward, not something to "plug-n-pray" about.
Want more tips, tricks, and insights to level up your software game? Sign up for the Test Double Dispatch and get the latest resources delivered right to your inbox.