Hexagonal
Architecture 

in Elixir
Buzzwords
🐝
Hexagonal
Architecture
Domain-Driven Design
Clean Architecture
Ports & Adapters
Lots of
Buzzwords
This is not about them…
Separate Domain
& Infrastructure
= maintainable software
@nicoespeon
Domain 

vs
Infrastructure
Domain
Business Logic
Infra.
Technical Details
Software is 

a mean 

to a 

Business end
Infrastructure is
a detail
Business 

doesn't care!
Infrastructure can change
shouldn't
Can your PM 

🌟 understand 🌟 

the source code?
Problems mixing
Domain & Infra.
def prs_open_time_until_today() do
today = Date.utc_today() !|> Date.to_iso8601()
{200, prs, _} =
Tentacat.Client.new(%{access_token: @access_token})
!|> Tentacat.Pulls.list("nicoespeon", "my-repo")
prs
!|> Enum.map(fn pr !-> pr["created_at"] end)
!|> Enum.flat_map(fn created_at !->
case DaysDiff.between(today, created_at) do
{:ok, open_time} !-> [open_time]
{:error, _} !-> []
end
end)
end
def prs_open_time_until_today() do
today = Date.utc_today() !|> Date.to_iso8601()
{200, prs, _} =
Tentacat.Client.new(%{access_token: @access_token})
!|> Tentacat.Pulls.list("nicoespeon", "my-repo")
prs
!|> Enum.map(fn pr !-> pr["created_at"] end)
!|> Enum.flat_map(fn created_at !->
case DaysDiff.between(today, created_at) do
{:ok, open_time} !-> [open_time]
{:error, _} !-> []
end
end)
end
Hard to 

see

the Business
Hard to 

test

the Business
Hard to 

change

the Business
Hard to change the Infra.
without breaking the Business
Is your Business
easy to read?
📖
Is your Business
easy to test?
🚦
Hexagonal
Architecture
The simplest way to



Separate Domain 

& Infrastructure
Infra.
Domain
The outside depends
on the inside.


Never the opposite.
Rule
Infra.
Domain
Dependency
Infra.
Domain
Dependency
Interface
Adapter
use
implement
Port
Adapter
use
implement
Ports & Adapters
Architecture
Inside the Hexagon
Business language 

only
%
Left-side Right-side
⚙
W
eb
UI
REST
API
Database
Send
event
'(Benefits
Flexibility
REST API CLI CRON
FTP File System HTTP
Console Sentry Postgres
…
You can plug another
Infra. to the Domain
🔌
A good software
architect defers
technical decisions
You can start with
something simple✨
In Elixir
Interfaces in Elixir


But we have
Behaviours
# i_read_prs.ex (Behavior)

defmodule IReadPrs do
@callback opened_prs() !:: [OpenTime.pr()]
end
# i_give_dates.ex (Behavior)

defmodule IGiveDates do
@callback today() !:: String.t()
end
defmodule OpenTime do
@type pr !:: %{created_at: String.t()}
def prs_open_time_until_today() do
end
end
defmodule OpenTime do
@type pr !:: %{created_at: String.t()}
@dates_giver Application.get_env(:pr_metrics, :dates_giver)
def prs_open_time_until_today() do
today = @dates_giver.today()
end
end
defmodule OpenTime do
@type pr !:: %{created_at: String.t()}
@dates_giver Application.get_env(:pr_metrics, :dates_giver)
@prs_reader Application.get_env(:pr_metrics, :prs_reader)
def prs_open_time_until_today() do
today = @dates_giver.today()
prs = @prs_reader.opened_prs()
end
end
defmodule OpenTime do
@type pr !:: %{created_at: String.t()}
@dates_giver Application.get_env(:pr_metrics, :dates_giver)
@prs_reader Application.get_env(:pr_metrics, :prs_reader)
def prs_open_time_until_today() do
today = @dates_giver.today()
prs = @prs_reader.opened_prs()
prs
!|> Enum.map(fn pr !-> pr.created_at end)
!|> Enum.flat_map(fn created_at !->
case DaysDiff.between(today, created_at) do
{:ok, open_time} !-> [open_time]
{:error, _} !-> []
end
end)
end
end
# dates_giver.system.ex (Adapter)
defmodule DatesGiver.System do
@behaviour IGiveDates
end
# prs_reader.github.ex (Adapter)
defmodule PrsReader.GitHub do
@behaviour IReadPrs
end
# dates_giver.system.ex (Adapter)
defmodule DatesGiver.System do
@behaviour IGiveDates
def today, do: Date.utc_today() !|> Date.to_iso8601()
end
# prs_reader.github.ex (Adapter)
defmodule PrsReader.GitHub do
@behaviour IReadPrs
end
# dates_giver.system.ex (Adapter)
defmodule DatesGiver.System do
@behaviour IGiveDates
def today, do: Date.utc_today() !|> Date.to_iso8601()
end
# prs_reader.github.ex (Adapter)
defmodule PrsReader.GitHub do
@behaviour IReadPrs
@access_token Application.get_env(:pr_metrics, :github_access_token)
def opened_prs do
{200, prs, _} =
Tentacat.Client.new(%{access_token: @access_token})
!|> Tentacat.Pulls.list("nicoespeon", "my-repo")
prs
!|> Enum.map(fn pr !-> %{created_at: pr["created_at"]} end)
end
end
# config/config.exs
use Mix.Config
config :pr_metrics,
dates_giver: DatesGiver.System,
prs_reader: PrsReader.GitHub
# config/config.test.exs
use Mix.Config
config :pr_metrics,
dates_giver: Mock.DatesGiver,
prs_reader: Mock.PrsReader
defmodule OpenTimeTest do
use ExUnit.Case
import Mox
end
defmodule OpenTimeTest do
use ExUnit.Case
import Mox
test "returns an empty array if there is no opened PR" do
open_times = OpenTime.prs_open_time_until_today()
assert open_times !== []
end
end
defmodule OpenTimeTest do
use ExUnit.Case
import Mox
@today "2011-01-10"
test "returns an empty array if there is no opened PR" do
Mock.DatesGiver
!|> stub(:today, fn !-> @today end)
open_times = OpenTime.prs_open_time_until_today()
assert open_times !== []
end
end
defmodule OpenTimeTest do
use ExUnit.Case
import Mox
@today "2011-01-10"
test "returns an empty array if there is no opened PR" do
Mock.DatesGiver
!|> stub(:today, fn !-> @today end)
Mock.PrsReader
!|> stub(:opened_prs, fn !-> [] end)
open_times = OpenTime.prs_open_time_until_today()
assert open_times !== []
end
end
"Program to a
behaviour"
Elixir
Use 

Domain language 

in Behaviours
+
Limits,
It doesn't guide you

to scale 🚀
Compatible

Has more layers
Compatible

Ubiquitous Language 

Bounded Contexts

Domain modelling
Separate Domain
& Infrastructure
🙏
Twitter 

GitHub 

Medium
}@nicoespeon
Nicolas Carlo

Hexagonal architecture & Elixir