Systems Thinking
Vendor-Neutral Design Lives or Dies at the Adapter Boundary
A vendor-neutral core stays clean only when vendor capabilities, errors, identifiers, and lifecycle quirks are translated at a deliberate adapter boundary.
- Systems Architecture
- API Design
- Vendor Abstraction
- Domain Models
- Integration Patterns
A vendor-neutral core stays clean only when vendor capabilities, errors, identifiers, and lifecycle quirks are translated at a deliberate adapter boundary. Move that translation work inward and the core becomes a maze of conditionals, version checks, and vendor-specific error handling. Move it nowhere at all and the caller becomes the maze instead.
The boundary is not a wrapper, a factory, or a naming convention. It is a deliberate contract that says: on this side of the line, we speak the language of the problem domain. On the other side, we speak the language of the vendor. Neither language leaks across. That contract has to be enforced, tested, and documented as carefully as any public API.
Canonical Model First
Define the canonical domain model before you write a single adapter. This model holds the entities, state transitions, and business rules that matter in your system—independent of how any vendor represents them.
When designing device orchestration, for example, the canonical model might include:
- A device lifecycle:
discovered,provisioning,ready,retiring - A capability set: what the device can and cannot do
- An identity: how you refer to it, not how the vendor refers to it
- A configuration envelope: the shape of configuration, not the vendor’s syntax
This model should be small enough to hold in your head and stable enough to survive a vendor change. If your core domain model includes vendor-specific fields, you have already lost neutrality.
The canonical model becomes your acceptance criterion for every adapter. An adapter is correct when it translates vendor reality into canonical reality without loss or invention.
Capability Discovery Belongs at the Boundary
Each vendor advertises what it can do in a different shape. One uses a capabilities array with strings. Another uses feature flags nested in a config object. A third returns capabilities as the set of operations that do not immediately fail.
The adapter is responsible for collecting that vendor shape, normalizing it, and surfacing it as a canonical capability set that the core can reason about. The core never learns the vendor’s capability language.
This boundary work includes:
- Mapping vendor capability names to canonical names
- Testing whether a capability is actually present or just documented
- Handling partial capabilities: “it can do X, but only under these constraints”
- Detecting capability drift during the lifetime of the connection
When the core needs to know if a device can reboot, it asks the abstraction. The abstraction answers yes or no. It never returns a vendor-specific string that the core has to parse.
Error Translation Is Not Optional
Vendors fail in vendor-specific ways. One times out, another returns a structured error with a code and nested reason. A third logs it and hangs. The adapter must translate all of these into a canonical error set.
That error set should answer the questions your system actually asks:
- Is this a transient failure (retry might work)?
- Is this a permanent failure (this device is broken)?
- Is this a constraint violation (you asked for something impossible)?
- Is this an authentication or trust problem?
- Is this an unknown failure (we need to investigate)?
Building the translation layer forces you to think through what “failure” means in your domain. A device reboot that returns a timeout is not actually a failure; it is the expected behavior. A vendor that silently drops commands because you lack permission is a security problem, not a transient hiccup.
Document each translation rule. When a vendor error code maps to canonical failure reason X, write it down with the reasoning. When the vendor’s behavior changes in a patch release, you need to be able to re-examine that mapping without rewriting the core.
Escape Hatches for Vendor-Specific Features
Some features are genuinely vendor-specific and valuable. A vendor might support firmware rollback that another cannot. A vendor might expose telemetry that is unique to its hardware.
The escape hatch pattern lets the core remain neutral while callers can opt into vendor depth:
- Define a
VendorSpecificorAdapterMetafield on canonical objects that holds opaque, vendor-scoped data - Require callers to test for vendor name and check feature availability before using vendor-specific fields
- Document the contract: if you use this escape hatch, you are now bound to this vendor for this part of the workflow
- Never allow vendor-specific data to flow back into the core logic
The escape hatch should hurt a little. It should look different from the canonical API. That friction is intentional. It reminds you that you are leaving the neutral layer.
Adapter Validation Checklist
Before an adapter enters production, validate it against its boundaries:
- Canonical mapping: Does every vendor entity map to exactly one canonical entity? Is that mapping deterministic and lossless?
- Capability accuracy: Test capability discovery against the actual vendor in its current state. Do not trust documentation.
- Error coverage: Can you enumerate every error the vendor can return? Is each one classified into your canonical error taxonomy? Have you tested each path?
- State consistency: When the vendor’s state changes outside your control, does the adapter detect it? Does it translate it correctly?
- Escape hatch safety: Does vendor-specific data ever influence canonical decisions? Is that data marked as such?
- Lifecycle edge cases: Adapter creation, reconnection after network loss, vendor version mismatch, partial feature support—test them all.
- Dry run semantics: If your system supports dry runs, does the adapter translate that through to the vendor correctly? Some vendors have no concept of dry-run; how do you handle that?
Testing Across the Boundary
Unit-test the canonical core without any adapter present. Use a test double that speaks canonical language only.
Integration-test the adapter in isolation. Give it vendor responses (real ones, captured and replayed) and verify it produces canonical outputs.
Integration-test the boundary with a real or simulated vendor, but make the test focused: does this specific capability discovery scenario work? Does this error scenario translate correctly? Use contract testing frameworks if they are available in your ecosystem.
Do not test the core and adapter together and call it done. That hides which side of the boundary failed.
When the Boundary Fails
If the core has to know vendor names, ask vendor-specific questions, or handle vendor-specific errors, the boundary has failed. Refactor it before you add the next vendor.
If an adapter becomes a maze of vendor-specific heuristics and workarounds, that is not a failure of the adapter. It is a signal that the vendor’s abstraction level is incompatible with your canonical model. You have three choices: raise the canonical model’s abstraction level (making it less neutral), accept deeper vendor-specific knowledge (moving the maze into the adapter, not the core), or accept that this vendor is not a good fit.
Choose consciously. Document the choice. The alternative is to make the decision implicitly by letting abstractions leak, and then to spend months wondering why the core is full of vendor logic.
A clean adapter boundary is not free. It requires discipline in the design phase, diligence in the implementation, and restraint when you are tempted to “just add” a vendor-specific shortcut. The payoff is a core that survives vendor changes, a test suite that catches regressions at the boundary instead of in production, and an honest map of where your system is truly vendor-neutral and where it is not.