Welcome to the Structural patterns, where the question shifts from how objects are created to how they fit together. First up is the one you’ve almost certainly used already, maybe without naming it. Adapter lets your code speak its own language while a small class translates for a third party that speaks a different one.

   
Category Structural
Smell fixed S06: Checkout depends directly on the courier’s XML API
Code patterns/structural/adapter

The Scene

CornerStore ships through a courier called ShipFast. Every successful checkout books a consignment:

[060] checkout customer=cu-1 payment=card
  card charged A$300.33 for Ava
  ShipFast booked SF-0001 for o-1001 (30033 cents)

Notice the 30033 cents. CornerStore thinks in decimal dollars. ShipFast thinks in whole cents, and it only speaks XML. Someone has to translate. The question is where that translation lives.


The Smell

In the baseline, it lives in checkout, inside StoreConsole:

    // SMELL S06: checkout calls the courier client with XML and parses its response directly
    private string BookShipment(Order order)
    {
        var cents = Math.Round(order.Quote.Total * 100m, 0, MidpointRounding.AwayFromZero)
            .ToString("0", CultureInfo.InvariantCulture);
        var name = System.Security.SecurityElement.Escape(order.Customer.Name) ?? "";
        var address = System.Security.SecurityElement.Escape(order.Customer.Address) ?? "";
        var request = "<shipment><orderId>" + order.Id + "</orderId><recipient>" + name
            + "</recipient><address>" + address + "</address><amountCents>" + cents + "</amountCents></shipment>";
        var response = shipFastClient.CreateConsignmentFromXml(request);
        return XDocument.Parse(response).Root?.Element("tracking")?.Value ?? "";
    }

Rounding to cents, XML escaping, element names, response parsing. None of that is about selling things. It’s all about how one vendor’s API happens to work.

That matters the day ShipFast changes its API, or the business adds a second courier. Your checkout code changes because a vendor did. Vendor details have leaked into the heart of your system.


The Idea

An Adapter sits between your code and someone else’s. Your code calls an interface that you designed, in your own terms. The adapter implements that interface by translating to and from the other side.

It’s the travel adapter in your suitcase. Your laptop charger doesn’t change when you land in another country. The wall socket doesn’t change either. A small plug in the middle makes them fit.


The Refactor

First, CornerStore defines what it actually wants from a courier, in its own language:

internal interface IShippingProvider
{
    string BookShipment(Order order);
}

Give it an order, get back a tracking number. No XML, no cents.

Then the adapter implements that interface by wrapping the existing ShipFast client:

internal sealed class ShipFastAdapter(ShipFastClient client) : IShippingProvider
{
    public string BookShipment(Order order)
    {
        var cents = Math.Round(order.Quote.Total * 100m, 0, MidpointRounding.AwayFromZero)
            .ToString("0", CultureInfo.InvariantCulture);

And checkout shrinks to one line in domain language:

order.TrackingNumber = shippingProvider.BookShipment(order);

If the adapter’s body looks familiar, it should. It’s almost exactly the code that used to live in StoreConsole. That’s typical of Adapter. You’re rarely writing new logic. You’re moving translation logic to the boundary, where it belongs, and giving the rest of the system a clean contract to depend on.

Also note what didn’t change: ShipFastClient itself. In real life, that’s often a NuGet package or vendor SDK you can’t modify anyway. Adapter works around it rather than through it.

classDiagram
    StoreConsole --> IShippingProvider : books shipment
    IShippingProvider <|.. ShipFastAdapter
    ShipFastAdapter --> ShipFastClient : translates request and response

The book calls IShippingProvider the target, ShipFastAdapter the adapter and ShipFastClient the adaptee.


What It Cost

Small and focused: 5 files, about 94 lines added and 17 removed. Most of the additions are tests and the new interface.

The payoff is out of proportion to the cost. Adding a second courier is now a second adapter, with no change to checkout at all.


When Not to Use It

  • The interface already fits. If a library’s API is already a good match for your domain, wrapping it adds a layer that translates nothing.
  • You’re wrapping for the sake of wrapping. “Wrap every third-party call just in case” leads to thin adapters that mirror the vendor API one-to-one. That gives you all the indirection and none of the protection. An adapter earns its place when there’s a real mismatch to absorb.

Easily Confused With

  • Bridge. Both put an interface between two sides, but for different reasons. Adapter makes two existing things fit after the fact. Bridge is designed up front so two dimensions can vary independently. See Adapter vs Bridge.
  • Decorator. A decorator keeps the same interface and adds behaviour. An adapter changes the interface.
  • Facade. A facade simplifies a whole subsystem behind a new entry point. An adapter converts one interface into another.

Try It Yourself

dotnet build patterns/structural/adapter/Adapter.slnx
dotnet test patterns/structural/adapter/Adapter.slnx
dotnet run --project patterns/structural/adapter/src/CornerStore/CornerStore.csproj -- scenarios/trading-day.json

Exercise: write a fake IShippingProvider that returns "TEST-001" and use it in a test. Notice how easy it is now to test checkout without any XML at all.

The Adapter README has the participant table and a sequence diagram. The full change is in DIFF.md.


Previous: Singleton · Next: Bridge