SwadhyAI Notes

All Low-Level Design notes

Structural · one foreign type

Adapter

An is a structural pattern that makes one existing class match an interface your code already uses. You keep the foreign class as-is, implement your interface on a small wrapper, and convert arguments and results on the way through.

A concrete picture: Checkout already talks to a such as Payable. The vendor SDK is the . The adapter’s only job is — for example Money to cents — not discounts, retries, or inventory checks.

Your type

Payable.pay(Money, id)

Adapter

StripePayableAdapter

rename · convert · forward

Vendor SDK

charge(token, cents)

Core question

Does this class already do the job — but speak the wrong method language?

Use Adapter when the answer is yes. If the pain is “the caller must talk to too many collaborators,” that is Facade. If you want extra behavior on the same type, that is Decorator. If you need lazy create, access control, or a remote stub, that is Proxy.

Before and after: one collaborator, new method shape

After you add an adapter, Checkout still has one payment collaborator. Only the method list changes. This means you can tell Adapter from Facade at a glance: Facade reduces how many types the caller sees; Adapter changes the shape of a single type.

What Checkout cannot call

stripe.charge(token, cents)
legacy.output(byte[] raw)
sdk.sendSms(msisdn, gsm7)

Vendor names, vendor units, vendor return types. Editing Stripe is usually not an option.

What Checkout already calls

payable.pay(Money, orderId)
printer.print(Receipt)
channel.notify(Phone, Message)

Your names and your value objects. Tests inject a fake Payable. Production injects the adapter.

The translation table — this is the whole class

Your typeVendor typeWhat the adapter maps
Money amountlong centsamount.toCents()
String orderIdidempotency key / metadatapass through or wrap
card token you holdString tokenconstructor field, not a domain type
PaymentResultStripeChargefrom(charge.id(), charge.ok())

If a row looks like “compute tax” or “retry three times,” it does not belong in this table. Those are not translations; they are policy or extra behavior.

Object adapter, not a subclass of Stripe

— compose, then implement

Hold the SDK. Implement Payable. Inject a fake StripeClient in tests. This is the Java shape. A that extends the vendor type couples you to internals you do not own and is almost never worth it in Java.

  1. 01Name Payable (your interface).
  2. 02Leave StripeClient untouched.
  3. 03Map Money and PaymentResult in pay().
  4. 04Inject the adapter where Checkout expected Payable.

Java sketch: a payment SDK becomes Payable

Checkout never imports StripeClient. Tests pass a fake Payable. Production wires StripePayableAdapter. This is dependency inversion in practice: the domain depends on the Payable abstraction, not on the vendor type.

java
interface Payable {
  PaymentResult pay(Money amount, String orderId);
}

class StripeClient {
  StripeCharge charge(String token, long cents) { /* vendor SDK */ }
}

class StripePayableAdapter implements Payable {
  private final StripeClient stripe;
  private final String token;

  StripePayableAdapter(StripeClient stripe, String token) {
    this.stripe = stripe;
    this.token = token;
  }

  public PaymentResult pay(Money amount, String orderId) {
    StripeCharge charge = stripe.charge(token, amount.toCents());
    return PaymentResult.from(charge.id(), charge.ok());
  }
}

// checkout = new Checkout(new StripePayableAdapter(stripe, token));

Where it sits — still one collaborator

caller

Checkout

target

Payable

adapter

StripePayableAdapter

adaptee

StripeClient

After the adapter, Checkout still has one payment friend. A crowd of Inventory + Payable + Invoice + Mailer is a different smell — use Facade for that. Retry around Payable is Decorator. A stand-in that constructs StripeClient on first pay() would be a virtual Proxy, and you almost never want that for a payment SDK you already hold.

Four wrappers that are not Adapter

Facade

Many familiar classes, one simpler method. placeOrder talks to four services. No type conversion is required for the pattern itself. See the Adapter vs Facade page.

Decorator

Same interface, extra behavior, and the inner object already exists. LoggingNotifier is still a Notifier, and you can stack wrappers. Use Adapter only when the wrapped type could not implement your interface without a translation.

Proxy

Same interface, but the goal is control of access, lifetime, or location. LazyImage is still an Image. You do not rename charge to pay; you decide when DiskImage is created.

Bridge

Two hierarchies you designed so they can vary independently. Adapter is after the fact: one class already exists and will not match. Timing and problem are different even if both “wrap.”

Common mistakes

Business rules inside the adapter

Discounts, “fail if inventory is short,” and retries are not mapping. The adapter converts types. Wrap Payable with a decorator after the adapter if you need retry.

Calling every wrapper an Adapter

LoggingNotifier is Decorator. LazyImage is Proxy. CheckoutFacade is Facade. Adapter is only when the wrapped type could not implement your interface without translation.

An adapter that becomes the checkout

If StripePayableAdapter also reserves stock and sends mail, you hid a use case inside a translator. Split the work: adapter for the SDK, facade or application service for the full checkout sequence.

Leaking StripeCharge to Checkout

If pay() returns StripeCharge, you did not adapt. Return PaymentResult. The point of the target interface is that the domain never names the vendor.

When Adapter is the wrong tool

You control both sides

If you can rename the SDK wrapper you wrote last week, just change the method. An adapter for your own class is unnecessary ceremony.

You need a new feature

Logging, compression, or caching a get are same-type extra work. That is Decorator. Do not name it StripeAdapter.

The caller juggles five types

Translating one of them does not shrink the sequence. Give the caller a facade. Adapt the vendor only where the signature itself is foreign.

How to explain this in an interview

“Stripe cannot be Payable without a translation, so this is an object adapter: compose StripeClient, map Money to cents, return PaymentResult. It is not a facade — we still have one collaborator. It is not a decorator — we are not adding retry here.”
  1. A legacy logger writes byte[]. Your app has LogSink.write(LogEvent). Sketch the adapter and the mapping table.
  2. Someone names a class StripeFacade that only renames charge to pay. What do you call it, and why?
  3. Checkout needs retry and a Stripe SDK. Where does Adapter sit, and where does Decorator sit?

Takeaway

Adapter connects one foreign class with the wrong methods to your interface. Translate types and forward the call. Leave policy, stacking, access control, and “too many collaborators” to Facade, Decorator, and Proxy.