Intent
Convert the interface of an existing class into another interface your client code expects. Adapter lets two classes work together that otherwise couldn't, because the seams don't fit.
The Problem
You've shipped your billing module. It speaks a clean internal interface:
interface PaymentGateway
{
public function charge(int $amountCents, Card $card): ChargeResult;
public function refund(string $chargeId): RefundResult;
}
Now the business says: "We're switching from our internal mock processor to Stripe in two weeks." You install the Stripe SDK and discover its API speaks a completely different language:
\Stripe\Charge::create([
'amount' => 1999,
'currency' => 'usd',
'source' => $tokenString,
'description' => 'Order #1234',
]);
Different parameter names. Different return shapes. Different error class hierarchy. Three options stand in front of you:
- Rewrite billing to call Stripe directly everywhere. Now Stripe is glued to every controller, every test, every test fixture. When you switch to PayPal in 2027, you do this again.
- Add
if ($gateway === 'stripe')branches inside billing. The class swells, tests multiply, and the next gateway compounds the mess. - Wrap Stripe in a small object that implements
PaymentGateway. The billing code never learns Stripe exists. Switching gateways becomes a one-line constructor change.
Option 3 is Adapter.
The Solution
Adapter says: write a class that implements the interface your code already expects (PaymentGateway) and translates each call into whatever the third-party library wants underneath. The third-party SDK and your client code never meet.
final class StripeGatewayAdapter implements PaymentGateway
{
public function __construct(private \Stripe\StripeClient $stripe) {}
public function charge(int $amountCents, Card $card): ChargeResult
{
$stripeCharge = $this->stripe->charges->create([
'amount' => $amountCents,
'currency' => 'usd',
'source' => $card->token,
]);
return new ChargeResult(
id: $stripeCharge->id,
success: $stripeCharge->status === 'succeeded',
);
}
public function refund(string $chargeId): RefundResult { /* ... */ }
}
The billing module imports PaymentGateway. It receives a StripeGatewayAdapter (or a PayPalGatewayAdapter, or a FakeGatewayAdapter in tests). It never imports anything from Stripe\. The adapter is the only place in your codebase that knows Stripe exists.
Real-World Analogy
A power outlet adapter. Your laptop charger has a Type-A plug; the wall socket in Berlin is Type-F. Neither the charger nor the wall is going to change for you. So you slot a small adapter between them: Type-A on one side, Type-F on the other. The current flows through unchanged; only the shape of the connection has been translated.
The adapter doesn't generate electricity. It doesn't transform voltage (that's a different device — a transformer). It just makes two incompatible interfaces fit together long enough for the real work to happen.
Structure
Three roles you'll see in every Adapter implementation:
- Target — the interface your client code already expects. Here:
PaymentGateway. - Adaptee — the existing class you can't (or won't) change. Here:
Stripe\StripeClient. - Adapter — the class that implements Target and holds an Adaptee, translating between them. Here:
StripeGatewayAdapter.
The defining property: clients depend on Target, never on Adaptee. The Adapter is the only place where the two interfaces are visible at the same time.
Code Examples
Here's a Stripe adapter behind a PaymentGateway interface in five languages. Notice how the signature of charge() is identical to the rest of your code; only the body translates to Stripe's shape.
interface PaymentGateway {
charge(amountCents: number, card: Card): Promise<ChargeResult>;
refund(chargeId: string): Promise<RefundResult>;
}
export class StripeGatewayAdapter implements PaymentGateway {
constructor(private stripe: Stripe) {}
async charge(amountCents: number, card: Card): Promise<ChargeResult> {
const stripeCharge = await this.stripe.charges.create({
amount: amountCents,
currency: "usd",
source: card.token,
});
return {
id: stripeCharge.id,
success: stripeCharge.status === "succeeded",
};
}
async refund(chargeId: string): Promise<RefundResult> {
const r = await this.stripe.refunds.create({ charge: chargeId });
return { id: r.id, success: r.status === "succeeded" };
}
}
from abc import ABC, abstractmethod
class PaymentGateway(ABC):
@abstractmethod
def charge(self, amount_cents, card): ...
@abstractmethod
def refund(self, charge_id): ...
class StripeGatewayAdapter(PaymentGateway):
def __init__(self, stripe):
self._stripe = stripe
def charge(self, amount_cents, card):
stripe_charge = self._stripe.Charge.create(
amount=amount_cents,
currency="usd",
source=card.token,
)
return ChargeResult(id=stripe_charge.id, success=stripe_charge.status == "succeeded")
def refund(self, charge_id):
r = self._stripe.Refund.create(charge=charge_id)
return RefundResult(id=r.id, success=r.status == "succeeded")
public interface PaymentGateway {
ChargeResult charge(long amountCents, Card card);
RefundResult refund(String chargeId);
}
public final class StripeGatewayAdapter implements PaymentGateway {
private final Stripe stripe;
public StripeGatewayAdapter(Stripe stripe) {
this.stripe = stripe;
}
@Override
public ChargeResult charge(long amountCents, Card card) {
Charge stripeCharge = stripe.charges().create(Map.of(
"amount", amountCents,
"currency", "usd",
"source", card.token()
));
return new ChargeResult(stripeCharge.getId(), "succeeded".equals(stripeCharge.getStatus()));
}
@Override
public RefundResult refund(String chargeId) {
Refund r = stripe.refunds().create(Map.of("charge", chargeId));
return new RefundResult(r.getId(), "succeeded".equals(r.getStatus()));
}
}
<?php
namespace App\Billing;
interface PaymentGateway
{
public function charge(int $amountCents, Card $card): ChargeResult;
public function refund(string $chargeId): RefundResult;
}
final class StripeGatewayAdapter implements PaymentGateway
{
public function __construct(private \Stripe\StripeClient $stripe) {}
public function charge(int $amountCents, Card $card): ChargeResult
{
$stripeCharge = $this->stripe->charges->create([
'amount' => $amountCents,
'currency' => 'usd',
'source' => $card->token,
]);
return new ChargeResult(
id: $stripeCharge->id,
success: $stripeCharge->status === 'succeeded',
);
}
public function refund(string $chargeId): RefundResult
{
$r = $this->stripe->refunds->create(['charge' => $chargeId]);
return new RefundResult(id: $r->id, success: $r->status === 'succeeded');
}
}
package billing
import "github.com/stripe/stripe-go/v76"
type PaymentGateway interface {
Charge(amountCents int64, card Card) (ChargeResult, error)
Refund(chargeID string) (RefundResult, error)
}
type StripeGatewayAdapter struct {
client *stripe.Client
}
func NewStripeGatewayAdapter(client *stripe.Client) *StripeGatewayAdapter {
return &StripeGatewayAdapter{client: client}
}
func (a *StripeGatewayAdapter) Charge(amountCents int64, card Card) (ChargeResult, error) {
sc, err := a.client.Charges.New(&stripe.ChargeParams{
Amount: stripe.Int64(amountCents),
Currency: stripe.String("usd"),
Source: &stripe.SourceParams{Token: stripe.String(card.Token)},
})
if err != nil {
return ChargeResult{}, err
}
return ChargeResult{ID: sc.ID, Success: sc.Status == "succeeded"}, nil
}
func (a *StripeGatewayAdapter) Refund(chargeID string) (RefundResult, error) {
r, err := a.client.Refunds.New(&stripe.RefundParams{Charge: stripe.String(chargeID)})
if err != nil {
return RefundResult{}, err
}
return RefundResult{ID: r.ID, Success: r.Status == "succeeded"}, nil
}
In every language the shape is the same: implement your interface, hold their object, translate inside the body. The billing layer never imports Stripe types — only the adapter file does.
When to Use It
Reach for Adapter when you can answer "yes" to any of these:
- You're integrating a third-party SDK. Payment, email, search, storage, geocoding — anywhere you'd otherwise be coupled to a vendor's class names.
- You're integrating with legacy code you can't change. Old internal services with weird signatures, frozen APIs, code from a previous era — Adapter shields the new code from the old shapes.
- You want to swap implementations later. If "switch from X to Y" is a probable future requirement, the adapter you write today is the swap point.
- You want testable code. With an adapter behind an interface, your tests inject a fake adapter and never touch the real third-party library.
If you have one call to a third-party library in one place and you'll never replace it, an adapter is overkill — just call the library.
Pros and Cons
Pros
- Your client code stays clean and free of vendor types.
- Swapping vendors becomes a one-class change.
- Tests can substitute a fake adapter trivially.
- The translation logic lives in exactly one place — no scattered conversions.
- The adapter file is the only file that imports the third-party library, which makes dependency audits much easier.
Cons
- One more class to maintain — and the adapter has to keep up with both interfaces (yours and the vendor's) when either changes.
- Adapters can leak vendor concepts upward if you're not careful (e.g., wrapping a Stripe error and re-throwing it loses information unless you map error codes too).
- Over-adapting is a real risk — wrapping every method of a 200-method SDK that you only need three of is wasted effort.
Pro Tips
- Adapt only what you use. Don't try to mirror the entire vendor API in your adapter. If your client code calls
charge()andrefund(), those are the only methods on the adapter. Add more when a real caller appears. - Keep the adapter dumb. It translates types and forwards calls. It does not add caching, retries, or logging — those belong in Decorator wrappers around the adapter, not inside it.
- Map errors, not just data. A
Stripe\Exception\CardExceptionshould become aPaymentDeclinedExceptionfrom your domain. The whole point is to keep vendor types out of the rest of the codebase. - Write a fake adapter for tests early. A
FakePaymentGatewaythat lets you script "next charge succeeds / fails / times out" beats mocking the vendor SDK every time. - Name the adapter after the vendor, not after "Adapter".
StripeGatewayreads better thanStripeGatewayAdapterwhen callers see it. The "Adapter" suffix is for diagrams; the actual class can drop it.
Relations with Other Patterns
- Decorator has the same wrapping shape but a different intent: a Decorator adds behavior to an object that already speaks the right interface; an Adapter translates an interface without adding behavior. If your wrapper has the same interface as what it wraps, it's a Decorator. If the wrapped object speaks a different interface, it's an Adapter.
- Facade also presents a clean interface to a subsystem, but Facade is about simplifying multiple objects; Adapter is about translating one. Use Facade when you want one method to hide ten; use Adapter when you want one shape to hide another.
- Bridge looks similar but is designed in advance — Bridge separates abstraction from implementation upfront; Adapter retrofits a translation onto code you didn't design together.
- Proxy also wraps an object with the same interface, but Proxy controls access (auth, lazy load, remote call) rather than translating shape.
Final Tips
The Adapter pattern is the cleanest investment you can make at the boundary of any third-party integration. Every time we've shipped one, future-us has come back and thanked past-us — when the vendor changed their API, when we needed a fake for tests, when we switched providers, when the security team needed to know exactly which files touched the SDK.
The cost is one class. The benefit is a sealed boundary between your code and theirs. When the next vendor change lands — and it always lands — you'll be glad the only file you have to open is the adapter.


