Index

Integration Without Illusions

A Capability Interface Is Smaller Than a Vendor Client

A controller needs to register an order webhook. It constructs a vendor SDK connection, accesses a dynamic resource property, calls its creation method, names the remote event, chooses response fields, and catches the SDK’s base exception.

Its test needs to intercept construction of that concrete connection and mock a dynamic property chain. The two useful behavior tests are skipped with the same explanation: the code needs an interface and dependency injection.

The controller does not need a vendor client. It needs one capability:

register order-created notifications

That difference is the useful boundary.

A client exposes the remote system’s grammar

A vendor client is often a faithful model of its API. It may expose resources, method names, request fields, authentication placement, error codes, and response arrays.

That is valuable inside an adapter. It is expensive in application code.

The retained controller knew all of these remote decisions:

  • how to construct an authenticated connection;
  • which remote resource managed event listeners;
  • the exact remote event name;
  • which result fields to request;
  • the dynamic call shape used to create and delete a listener;
  • which SDK exception meant invalid credentials; and
  • which SDK exception meant the listener was already absent.

Replacing the SDK therefore required changing the controller even though its product behavior had not changed.

An application-facing interface should describe the operation the application owns:

registerOrderWebhook(account, callbackUrl) -> subscription
removeWebhook(account, subscriptionId) -> void

The adapter can translate that request into the vendor’s resource names, protocol, fields, and authentication.

Put the translation map in one place

The retained repair introduced a narrow webhook manager between two controllers and the remote connector.

connect controller ──┐
                     ├── webhook lifecycle capability
disconnect controller┘              |
                                    v
                              remote connector
                                    |
                                    v
                      authentication + JSON-RPC + errors

The connect controller now asks to create the one webhook the product needs. The manager supplies the vendor event name and requested result fields. The disconnect controller asks to remove a known subscription. Neither controller constructs a remote connection or navigates the SDK’s dynamic resources.

This is not abstraction for its own sake. It gives each decision one owner:

DecisionOwner
when a user connects or disconnectsapplication controller
which product event needs a webhooklifecycle capability
remote event name and response fieldsadapter
authentication placementconnector
request framing and endpointconnector and request object
remote error-code translationconnector
customer-facing failure responseapplication controller

When the remote API changes its event-listener spelling, the application use case does not need to change. When the product decides to register a second event, the capability changes deliberately rather than leaking another remote method call into a controller.

Translate failures before they cross the boundary

Transport libraries and SDKs fail in their own vocabulary. Application code should not need to know numeric vendor error codes or catch a package-specific exception.

The retained connector maps remote errors into three local categories:

access denied
remote object not found
integration request failed

That lets the application make its own decisions. Registration failure becomes a clear invalid-credentials result and no local integration record is created. During disconnection, “already absent remotely” is accepted and local cleanup continues.

Those two policies are different even if both failures came from the same remote endpoint. The adapter identifies what happened; the application decides what that outcome means for its workflow.

Do not flatten every failure into IntegrationException. Callers need distinctions they can act on. Do not reproduce every vendor error class either. A useful local taxonomy is the smallest set which changes retry, compensation, customer response, or local state.

Test the two sides of the seam differently

Before the repair, the controller tests tried to overload a concrete SDK class and configure a dynamic call chain. They were skipped because the dependency could not be substituted cleanly.

After the repair, the application tests supply a small in-memory capability:

register(...) -> returns synthetic subscription metadata
remove(...)   -> records the removed subscription ID

Those tests can assert product behavior:

  • successful registration stores the returned subscription identity;
  • registration failure stores no local integration;
  • disconnection asks to remove the right subscription;
  • local state is removed after remote absence is tolerated.

Connector tests own a different contract:

  • base URL and authentication query are correct;
  • JSON-RPC method and parameters are correct;
  • requested response fields are correct; and
  • remote error codes become the expected local exceptions.

The controller test no longer cares whether the adapter uses an SDK, a generic HTTP client, or a hand-written socket. The connector test no longer needs a database, route, session, or feature flag.

That separation made the two previously skipped behavior tests executable. It does not prove the real remote service accepted the requests; the retained connector tests use mocked HTTP responses.

The retained seam is useful, not finished

The new interface is narrower than the vendor client, but it still leaks two weak shapes.

First, registration returns an untyped array. The controller reaches into it for the remote subscription identity. A stronger result would name the local facts:

WebhookSubscription {
    id: string
    status: SubscriptionStatus
}

Second, every method receives raw account credentials. That may be appropriate if the capability is deliberately stateless, but it spreads secret-bearing arguments across callers. An account reference resolved inside the adapter can reduce exposure when the application’s credential model supports it.

The interface also names one product event in the method itself. That is a good constraint while the product supports exactly one webhook. It becomes awkward if many event types share the same lifecycle and policy. At that point a typed event enum or subscription specification may be the better capability.

These are reasons to improve the seam, not reasons to expose the entire remote client again.

Keep broad clients at the edge

A broad client interface can be reasonable when the application truly provides a generic proxy or administration console for most of the remote API. In that case, mirroring remote resources may be the product.

Direct SDK use can also be reasonable in a short-lived migration script or a small process whose only responsibility is that integration. The cost appears when remote grammar crosses into code which owns unrelated product policy.

A generated SDK does not remove the boundary decision. It can provide excellent serialization, authentication, and endpoint coverage while remaining behind a capability adapter. The question is not whether the SDK is good. It is whether application callers should inherit its model and release cycle.

Review the leak map, not the interface count

Adding an interface does not guarantee containment. Review the actual dependencies on both sides:

ConcernContained?Evidence to seek
remote package classesyes when imports stop at the adapterdependency scan
authentication formatyes when callers pass local account contextboundary test
endpoint and request grammaryes when request objects own themconnector test
error codes and package exceptionsyes when mapped locallyfailure tests
response terminologyonly if results are typed locallyreturn type and consumers
webhook lifecycle policyyes when capability names product operationsapplication tests
credentialsonly if exposure is intentionally boundeddata-flow review

The retained migration removed four direct vendor packages across several integrations, but that count is not the argument here. INT-04 already examines semantic translation in one payment path, and the later design retrospective uses the broader replacement as a change-pressure test. This article owns the narrower controller-to-webhook capability boundary.

The public reproduction runs a connect and disconnect workflow against a fake capability, then separately exercises a synthetic adapter’s request framing and error mapping. It does not reproduce the private SDK, HTTP library, provider, controller framework, or production integration.

An adapter earns its place when application code can state what it needs without speaking the remote system’s language. Ask for the capability, keep vendor grammar at the edge, and test product decisions separately from wire translation.