Flexible Design with Adapters

With Phoenix 1.3 well behind us now, Elixir developers understand how to approach most problems “The Elixir Way”:

  • Create a well-named context module for the problem, e.g. “Payment”
  • Put functions in it, which don’t expose how data is persisted
  • Profit

This is great, but it can be taken even further. A Payment module is a great example, in fact. Many developers1 would scan the landscape for the best payment gateway out there2, find its companion library in Hex, and build their whole Payment module around its API. They would use the gateway library’s structs throughout their application, and name fields in the database using gateway-specific terms.

This gets the job done, but it can be shortsighted. Eventually, the stakeholders always want to do crazy things like:

  • Switch payment gateways (“Authorize.net gave us a better deal!”)
  • Route some payments through gateway A and others through gateway B

This kind of change is very common whenever the third party service you’re integrating is generic and commoditized, i.e. offered by more than one vendor. For example:

  • File uploading and storage
  • Image manipulation
  • Payment processing
  • SMS delivery
  • Email delivery
  • Content-delivery networks
  • Analytics
  • Performance & error monitoring

If your app isn’t designed to anticipate this kind of change, you’ll be caught flatfooted whenever you need to change vendors for any of these services.

Introducing Adapters

In Elixir, the best way to insulate your app from vendor changes is to isolate your vendor-specific code into Adapter modules.

Before you get to that though, you’d first define app-specific structs for your problem space. For example, you might create a MyApp.Payment.Charge struct to represent credit card charges.

You’d use these structs exclusively throughout your app, never depending on vendor-specific structs or types.

%MyApp.Payment.Charge{
  gateway_id: "[id of charge on gateway]",
  amount_cents: 1_000,
  currency: "usd",
  # ...
}

Next, you’d identify which features you need from your vendor, and codify those into an Adapter behaviour. In this case, Gateway seems to be a good name.

defmodule MyApp.Payment.Gateway do
  alias MyApp.Payment.{Charge, GatewayError}

  @callback create_charge(Charge.t) :: {:ok, Charge.t} | {:error, GatewayError.t}
end

You’d then implement that behaviour for your chosen vendor.

defmodule MyApp.Payment.StripeGateway do
  @behaviour MyApp.Payment.Gateway

  @impl true
  def create_charge(charge) do
    # ...
  end
end

Inside your context, when you need to make a call to your vendor, you fetch an Adapter module from configuration and call it:

defmodule MyApp.Payment do
  def create_charge(attrs \\ %{}) do
    %MyApp.Payment.Charge{}
    |> struct(attrs)
    |> gateway().create_charge()
  end

  defp gateway do
    :my_app
    |> Application.get_env(__MODULE__, [])
    |> Keyword.fetch!(:gateway)
  end
end

Finally, you’d configure your context to use the desired Adapter.

config :my_app, MyApp.Payment,
  gateway: MyApp.Payment.StripeGateway

Congratulations, you now have an adapter-based design!

Benefits

Now that your vendor is isolated behind a behaviour, you can much more easily support multiple vendors. Beyond that, you also get a major benefit in your tests.

You can now easily implement a Test implementation of your Adapter which you only use in the :test environment. This allows you to thoroughly test all the possible error states, without having to make live API calls. This is particularly nice when your vendor doesn’t provide a good “test” version of their API.

Examples

In conclusion, here are some stand-out libraries on Hex which implement the pattern if you want to see it in action:

  • Ecto: Adapter-based relational database library
  • Swoosh: Adapter-based email delivery
  • Mouth: Adapter-based SMS delivery
  • Ueberauth: OAuth library, see the Strategy behaviour

  1. Myself included. [return]
  2. It’s Stripe by the way. Braintree is a close second. [return]
comments powered by Disqus