DEV Community

Diogo Dourado
Diogo Dourado

Posted on • Updated on

Dependency inversion on Elixir using Ports and Adapters design pattern

Introduction

Dependency Inversion is a well-known software development principle, described by Robert C. Martin as the responsible of systems to become flexible by making source code dependencies refer only to abstractions and not to concrete items.

Dependency Inversion demonstrations taken from Clean Architecture book
Dependency Inversion demonstration taken from Clean Architecture book.

Deciding which implementation will be used in a given piece of code can be very useful and is commonly used to develop mocks on tests that involves external dependencies, such as HTTP calls or event publications.

How it is used

On Java and other object oriented programming languages the principle can be achieved using an Interface to expose desired functions and then develop a Class to implement these signatures. On the other hand, Elixir makes it possible to apply this principle by defining a set of callbacks on a Behavior and implement them on another module following the defined specification.

We can see this pattern replicated on different open source libraries of Elixir community on many use cases:

Implementation example

By deriving the Dependency Inversion principle on Elixir it is possible to implement Ports and Adapters design pattern. It consists of having a Port defining the Behaviour callbacks and the functions calling the configured Adapter implementation.

Check out this quick demonstration:

defmodule MySystem.SmsNotificationPort do
  @moduledoc """
  Sends SMS to users using configured SMS adapter.
  """

  @callback notify(user :: User.t(), message :: String.t()) :: :ok | :error

  def notify(user, message), do: impl().notify(user, message)

  defp impl, do: Application.get_env(:my_system, __MODULE__)
end

defmodule MySystem.TwillioSmsAdapter do
  @moduledoc """
  Twillio SMS adapter implementation.
  """

  @behaviour MySystem.SmsNotificationPort

  @impl true
  def notify(user, message) do
    case MySystem.HTTP.post(http://twillio-sms.com/" <> user.phone, %{message: message}) do
      {:ok, _} -> :ok
      _ -> :error
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

And on config.exs we would setup:

# …

config :my_system, MySystem.SmsNotificationPort,
  MySystem.TwillioSmsAdapter
Enter fullscreen mode Exit fullscreen mode

Conclusion

The same principle is used on Dependency Injection design pattern, given that you can use different implementations on a piece of your software. The difference is that Dependency Injection implementation is given on compile-time whilst Ports and Adapters is on run-time.

This means you could execute the command below on a running application and change the SMS API used on your system.

Application.put_env(:my_system, MySystem.SmsNotificationPort, MySystem.ZenviaSmsAdapter)
Enter fullscreen mode Exit fullscreen mode

SMS Port implementation swap

This is very powerful because now we are able to switch running implementations on the fly. For instance, if Twillio went through an outage we could change the SMS implementation Adapter to Zenvia resulting in our users having zero impact on their SMS messages. In the future, we could automatically detect outage events on a specific SMS adapter and switch to the other one, a pattern known as Circuit Breaker.

Top comments (11)

Collapse
 
brunoribeiro147 profile image
Bruno Ribeiro

Hey man, great article!! I always try to implement this pattern on my code. One thing that a make different, is that I use defdelegate on the port, I think that make the code more easy to understand. But thanks for the excellent article

defmodule MySystem.SmsNotificationPort do
  @moduledoc """
  Sends SMS to users using configured SMS adapter.
  """

  @callback notify(user :: User.t(), message :: String.t()) :: :ok | :error

  @adapter Application.get_env(:my_system, __MODULE__)

  defdelegate notify(user, message), to @adapter
end
Enter fullscreen mode Exit fullscreen mode
Collapse
 
03juan profile image
Juan Barrios

Doesn't assigning it to @adapter make it a compiled variable, breaking the Application.put_env trick?

Collapse
 
dcdourado profile image
Diogo Dourado

Yes @03juan, didn't notice that

Collapse
 
hackvan profile image
Diego Camacho

I think you're right, in this case we need to move to a private function instead

defp adapter, do: Application.get_env(:my_system, __MODULE__)

Doesn't it?

Collapse
 
brunoribeiro147 profile image
Bruno Ribeiro

Yep! You right! I already change this implementation, and now I use Application.get_env instead

Collapse
 
dcdourado profile image
Diogo Dourado

Great tip, Bruno! Thanks for the feedback

Collapse
 
hackvan profile image
Diego Camacho • Edited

Hi Bruno,

I don't quite understand why the delegate or function call of the behavior should be made in the port, what's the porpuse?

It's because all the application use the MySystem.SmsNotificationPort.notify/2 function?

Collapse
 
brunoribeiro147 profile image
Bruno Ribeiro

Hey Diego!! How are you?

Well, I use the delegate on the port, because the port's idea is to be an interface. The only responsibility of the port is to call the correct adapter.

Collapse
 
lcezermf profile image
Luiz Cezer

Awesome content!

Collapse
 
rakshans1 profile image
Rakshan Shetty

Paring this with mox for testing makes testing so much simpler

Collapse
 
brunoribeiro147 profile image
Bruno Ribeiro • Edited

Yep! I like to use with Hammox! It's a really good abstraction for the mox