Skip to content
DedicatedPHP Contact

How to isolate external integrations in PHP without contaminating the domain

Learn how to encapsulate APIs and external systems in PHP with your own contracts, adapters, and a gradual plan to reduce coupling and risk.

Editorial diagram of a PHP application with an internal port and adapters isolating several external APIs

An integration stops being a technical detail when its fields, error codes, and access rules appear in controllers, application services, models, and business processes. At that point, changing providers, updating an API, or handling an external incident requires modifying parts of the application that should not know about that system.

Isolating external integrations in PHP means establishing a clear boundary: the domain expresses what it needs in its own language, and an infrastructure layer translates that need into the provider's specific protocol, format, and behavior. It is not about hiding an API behind a class with a different name, but about preventing its decisions from shaping the entire application.

When an integration is already contaminating the application

When an integration is already contaminating the application — DedicatedPHP visual guide

Coupling tends to grow incrementally. A team consumes an API from a controller to address an urgent delivery; then another flow reuses the same client; eventually, response arrays and SDK exceptions become implicit dependencies of the business.

  • Use cases receive or return arrays with provider field names.
  • Business logic compares external codes, such as ERR_42 or PENDING_REVIEW.
  • Controllers, commands, and queue jobs build HTTP requests directly.
  • Exceptions from the external SDK are caught outside the infrastructure layer.
  • A change to credentials, endpoints, or versions requires editing several modules.
  • Domain tests require real connections, tokens, or mocked provider responses.

These signs do not mean that a full rewrite should begin. They do indicate that the integration should be prioritized according to its risk: business criticality, frequency of provider changes, number of internal consumers, data sensitivity, and difficulty of recovering from a failure.

Defining contracts in the language of the business

The internal contract, also called a port, should describe a capability the application needs, not a reproduction of an external API's operation catalog. For example, a booking application may need to “request a reservation,” “check its status,” and “cancel it.” The domain does not need to know that a provider uses XML, OAuth, a numeric identifier, or a particular retry convention.

A port can be expressed as a PHP interface:

interface ReservationGateway
{
    public function request(ReservationRequest $request): ReservationResult;
    public function status(ReservationReference $reference): ReservationStatus;
    public function cancel(ReservationReference $reference): void;
}

The contract types must belong to the internal language. ReservationRequest contains the data needed for the business decision; it should not include authentication fields, HTTP headers, or legacy provider names. Likewise, an internal reference can encapsulate the external identity without making it dominant across all use cases.

The components of an integration boundary

Port, adapter, and internal DTO

The port is the interface consumed by the application. The adapter is the implementation that communicates with the external system. Between them, internal DTOs transfer data in a stable structure for the application.

The adapter translates in both directions: it converts the internal DTO into a specific request and normalizes the response into a result the domain can interpret. If the provider changes guest_count to travellers, the change should remain contained within that adapter.

Configuration, credentials, and transport

Endpoints, tokens, timeouts, certificates, and retry policies are infrastructure concerns. They must be injected through configuration and kept out of domain entities and services. It is also advisable to separate the HTTP client or SDK from the adapter: this makes it easier to replace libraries, record telemetry, and test mapping without depending on the actual transport.

Error translation and uncertain states

Not all failures require the same handling. A validation error rejected by the provider may be recoverable for the user; an authentication failure requires operational intervention; a timeout may leave an uncertain state because the provider may have processed the request.

The internal contract must represent these differences without leaking foreign exceptions. For example, the adapter can transform a validation response into ReservationRejected, a temporary issue into TemporaryUnavailable, and a timeout after sending the request into UnknownSubmissionState. The latter should not be treated as a simple error: it may require a subsequent lookup by an idempotency key or operational reconciliation.

Translating errors does not mean removing details. Safely log the correlation identifier, technical cause, and relevant response, while avoiding exposing secrets or sensitive data to the user.

Example of encapsulating a booking service

Suppose a provider requires a JSON request with dates in a specific format, its own hotel code, and an authorization header. The internal use case should not build that request. It receives a reservation request, applies its rules, and calls ReservationGateway.

The ExternalReservationAdapter adapter performs the specific tasks:

  • Converts the internal accommodation identifier into the code recognized by the provider.
  • Formats dates, guests, and preferences according to the external contract.
  • Adds credentials and an idempotency key.
  • Interprets HTTP codes, error bodies, and provider-specific statuses.
  • Returns an internal reference and status.

The application retains the rule for when a reservation is acceptable; the adapter retains the rule for how to request it from that provider. If a second provider is added, it can implement the same port as long as the business capability is equivalent. If it is not, forcing a common interface can hide important differences and create ambiguous decisions.

How to extract an already coupled integration

A safe migration does not require stopping product evolution. Start with an inventory: locate direct calls, SDK classes, external formats, leaked exceptions, and consumers. First identify the critical paths or those that change most frequently.

  1. Introduce a facade: create the port and an initial adapter that can temporarily reuse part of the existing client.
  2. Migrate consumers by flow: replace direct calls one use case at a time. Avoid maintaining two different interpretations of the same error.
  3. Centralize mapping: remove conversions of external fields and codes from controllers, services, and templates.
  4. Add observability: record latency, results, normalized errors, and correlation between the internal request and the external call.
  5. Remove direct access: when no consumers remain, restrict or remove the exposed client to prevent regressions.

During the transition, the facade must not become a generic container for SDK methods. Its purpose is to define a useful, stable boundary, not to move coupling into another folder.

Tests and criteria for validating isolation

Domain tests should use port test doubles. This lets them verify business decisions without network access, credentials, or incidental provider behavior. Adapter tests, on the other hand, should verify the mapping of requests, responses, and errors against a controlled environment, a mock server, or contracts documented by the external system.

The outcome is verifiable if these criteria are met:

  • A change to the format, endpoint, or SDK is concentrated in the adapter and its configuration.
  • Use cases depend on internal contracts, not HTTP clients or external types.
  • Provider exceptions and codes do not cross the boundary.
  • Uncertain states have explicit handling, including idempotency or subsequent lookup when necessary.
  • Business tests run with test doubles, and integration tests validate the actual translation.

Common mistakes before adding another provider

Common mistakes before adding another provider — DedicatedPHP visual guide

Premature abstraction is a risk: do not create a complex hierarchy for a single stable integration without real replacement needs. The opposite extreme also fails: replicating the entire external API in an internal interface makes the domain inherit its complexity.

Before integrating, confirm what capability the business needs, who owns each piece of data, which errors are actionable, how duplicate operations are prevented, and what will happen if no response arrives. Define the port around those decisions, implement the adapter as a translator, and keep external peculiarities at the edge. That discipline makes it possible to isolate external integrations in PHP without turning every provider change into a cross-cutting application change.

Want to apply these ideas to your project?Let’s discuss your PHP platform.
View related service