Adapter Pattern Using Protocols in Clojure

Learn how protocols and plain functions can normalize incompatible interfaces at system boundaries without letting conversion logic leak through the whole codebase.

Adapter pattern: A structural pattern that converts one interface into another so the rest of the system can work with a stable shape.

Adapters matter because real programs rarely control every input shape. A service may receive one vendor’s JSON payload, another team’s Java object, and a legacy map format from an older subsystem. If every caller has to understand all of those shapes, the system becomes brittle. An adapter creates one local boundary where outside forms are translated into the internal form the rest of the code expects.

In Clojure, the Best Adapter Is Often Small

In object-oriented examples, adapter often looks like a class hierarchy. In Clojure, it is usually simpler:

  • a protocol for a stable behavior boundary
  • a plain function that reshapes data
  • a namespace dedicated to external-to-internal translation

The goal is not to reproduce the classic UML diagram literally. The goal is to keep compatibility code at the edge.

Protocols Work Well When Behavior Must Vary by Type

Protocols are useful when different types need to satisfy one behavior contract.

 1(defprotocol Chargeable
 2  (charge-cents [payment amount-cents]))
 3
 4(defrecord StripePayment [client customer-id]
 5  Chargeable
 6  (charge-cents [_ amount-cents]
 7    {:provider :stripe
 8     :amount-cents amount-cents
 9     :status :submitted}))
10
11(defrecord LegacyGateway [endpoint account]
12  Chargeable
13  (charge-cents [_ amount-cents]
14    {:provider :legacy-gateway
15     :amount-cents amount-cents
16     :status :queued}))

The rest of the system can call charge-cents without caring whether the concrete type came from a modern SDK or a legacy wrapper.

Data Reshaping Is Often Enough

Sometimes you do not need polymorphism at all. If the real problem is data shape, a plain adapter function is clearer.

1(defn vendor-order->internal-order [payload]
2  {:order/id (:id payload)
3   :order/customer-id (get-in payload [:customer :external-id])
4   :order/total-cents (:amount_cents payload)
5   :order/status (keyword (:status payload))})

This is still adapter logic. It just uses data transformation instead of type-based dispatch.

Keep the Internal Model Stable

A strong adapter boundary protects the rest of the code from vendor churn. If one provider renames a field or changes a response format, only the adapter should need to change.

That means the internal shape should be:

  • business-oriented
  • stable across callers
  • free of vendor-specific naming where possible

If internal code is filled with legacy_, vendor-x_, and transport-specific field names, the adapter boundary is not doing its job.

Adapters Are About Containment, Not Indirection for Its Own Sake

Poor adapters often become little more than pass-through wrappers. Good adapters do real boundary work:

  • normalize naming
  • convert units and representations
  • turn exceptions into ordinary result data when useful
  • hide transport or SDK quirks

But they should not absorb business policy that belongs deeper in the application.

Common Failure Modes

Letting Vendor Shapes Spread Everywhere

Once raw external payloads leak through the system, every later change becomes more expensive.

Choosing Protocols When a Function Would Do

Protocols are useful, but not every field-mapping problem needs type dispatch.

Hiding Business Decisions Inside the Adapter

Conversion logic belongs at the edge. Pricing rules, authorization decisions, and workflow policy do not.

Practical Heuristics

Use adapters at boundaries where your system does not control the incoming shape. Prefer a plain transformation function when the problem is data normalization, and use protocols when behavior truly varies by concrete type. The best adapter is the one that keeps the rest of the code boring.

Ready to Test Your Knowledge?

Loading quiz…
Revised on Thursday, April 23, 2026