Fl4m3Ph03n1x
Hexagonal architecture in elixir
Background
For the longest time now I have been playing with the idea of doing an application that follows the hexagonal architecture:
After reading several books on the matter, this looks like something that everyone should do. It is hard for me to find a compelling reason to not use it given that you are writing any code with a decent amount of complexity (don’t use it for an Hello World app, obviously).
However, ironically, I have never found, in all my life as a programmer, any project using it. In fact, none of my colleagues I have (or ever had) even knew about it.
What now?
So, now I have decided with my free time, to create a pet project where I can implement this architecture.
This project should be simple: It is a command line app, that makes HTTP requests to an external website.
There is no database, authentication, no nothing. Just invoking the app:
# makes a hello world search in google and IO.puts the result
./my_app --greet="hello world"
So, out of the box, I know I need an adapter for an HTTP client (lets say, HTTPoison) and a JSON decoder (lets say Jason) because I want my app to be able to change between decoders.
Questions
And this is where I freeze. There is so much stuff in my head, I can’t even start.
How should I implement the port? Via an interface (module with callbacks) like Jose Valim in hix Mox article?
http://blog.plataformatec.com.br/2015/10/mocks-and-explicit-contracts/
What should that interface be like? Have GET and POST methods or have a “greet” method?
What about my adapters?
It really feels like that although I have read extensively about the topic, I am incapable of processing the information into something useful.
A code sample would really help.
Has any of you ever did something similar to this in Elixir?
Most Liked
tomekowal
I had a period when I studied Hexagonal, onion and other architectures and London vs Detroit schools. I find them all different ways to achieve the same thing: split business logic from implementation details.
There are two main benefits that I see:
- your business code usually changes more often than implementation details, so it is a less mental burden to change it when it doesn’t touch DB, HTTP params and what-not
- if you decide to change the database (a rather rare occurrence, but sometimes scaling requires this), stuff is less tangled, and you don’t break the business logic
What they don’t tell you is how are you going to pay for it.
Doing everything via the interface is another layer of abstraction. It is also easy to miss the rule and use the direct call in the code. Elixir doesn’t have interfaces. The closest thing is behaviours. They have quite a lot of boilerplate because it is a separate module usually in a separate file.
Plus, you need another layer of configuration for specifying which implementation to use (config.exs is a bad fit for that).
That’s why I like an approach described by Rafał Studnicki in this talk: https://www.youtube.com/watch?v=XGeK9q6yjsg He splits every use case into three sets of files: Model - pure business logic, IO - external dependencies like DB, and service - which coordinates. The rules are:
Models can’t use anything in IO or Service to not pollute business logic.
IO can use Models (mainly, if you read something from DB, you translate it to Model instead of returning raw schemas)
Services call IO and Model functions and can be the place to choose proper implementation (if there is one, direct calls are sufficient).
For projects I worked on, it is the lightest approach with the least amount of trade-offs.
There are, of course, trade-offs:
- typically, the web layer can extract information from Ecto.Schema and use it (have you wondered how Phoenix.Form decides on which form it should use PUT and POST?), if you use this approach, you need to distinguish yourself
- you define schemas for your DB and often define an almost identical struct for your Model
Any architectural patterns can pay off in more significant projects where you often jump between implementing different parts of business logic.
As for TDD with or without mocks. The SQL.Sandbox abstraction makes running tests asynchronously and in isolation so straightforward that not using real DB in tests seems wrong. There is no downside to doing it the simple way. I only use mocks for calling external services.
Apemb
I worked for the better part of a year on a Phoenix Application that ended up being something like a GraphQL - DDD - CQRS - Hexagonal like architecture.
It started a small innocent Phoenix HTML server but well thing got complex very fast and we had to structure it better than with contexts, plus we ended up needing a separate front-end app.
I quit last month to work elsewhere, but it was a good architecture. I now work on a node.js mess, and I regret it every day.
I do not know what exactly where I can help, but here is where the architecture ended up when I left. (the app is named Kairos)
(I did leave out some very specific and legacy stuff for clarity)
/kairos_domain <-- where the business logic lives
/aggregates <-- the aggregate modules + structs per DDD teachings
/services <-- other domain relatated logic
/kairos_command <-- the "write" part of the application
/commands <-- the commands dispatched from the GraphQL mutations mostly
/ports <-- the interfaces for the commands to interact with dependencies
/kairos_query <-- the "read" part of the application
/queries <-- the queries dispatched from the GraphQL query resolvers
/models <-- read structs
/kairos_infra <-- the infrastructure
/repositories <-- the repositories, getting data from the database, and adapting to domain aggregates
/tables <-- ecto structs
/adapters <-- the adapter modules to go from aggregates to ecto struct and the otherway around
/dataloaders <-- the dataloaders to read ecto structs with caching for better request with absinthe
/kairos_web <-- the application part
endpoint.ex <-- phoenix endpoint
router.ex <-- phoenix router
schema.ex <-- absinthe graphql schema
/schema <-- absinthe graphql types
/adapters <-- adapters between mutations and commands, or between query models and graphql objects
Port were implemented this way :
defmodule KairosCommand.Ports.ShiftAggregateRepository do
defmodule Behaviour do
alias KairosDomain.ShiftAggregate
@type uuid :: String.t()
@callback get(id :: uuid) ::
{:ok, ShiftAggregate.t()}
| {:error, {:resource_not_found, [name: :shift_aggregate, id: String.t()]}}
| {:error, term}
@callback save(ShiftAggregate.t()) ::
{:ok, ShiftAggregate.t()}
| {:error, {:validation_error, [KairosDomain.ValidationError.t()]}}
| {:error, {:resource_not_found, [name: :shift_aggregate, id: String.t()]}}
| {:error, term}
@callback delete(ShiftAggregate.t()) ::
{:ok, :deleted}
| {:error, {:resource_not_found, [name: :shift_aggregate, id: String.t()]}}
| {:error, term}
end
@behaviour Behaviour
@impl_module Application.get_env(
:kairos,
:shift_aggregate_repository,
KairosInfra.ShiftAggregateRepository
)
@impl Behaviour
defdelegate get(id), to: @impl_module
@impl Behaviour
defdelegate save(shift_aggregate), to: @impl_module
@impl Behaviour
defdelegate delete(shift_aggregate), to: @impl_module
end
For the tests, in the test config you can specify a mock module (using mox for example, we used a fork of mox named erzats for the job) to use only unit tests. It simlifies immensly your life when your database model is quite complexe as was ours. You can than return whatever you want from the repositories, and not having to insert tens of ecto structs in the right order.
We did nice things with the command module, structs that represented the command, and a protocol implementation for the command handler. But it is a bit out of scope.
Feel free to ask any questions you want. I do not know to help you on your discovery, but would love to help 
Apemb
The fact is we did not indent from the start to use Commands and Queries, but as we used GraphQL, it kinda appeared naturally.
GraphQL mutations are natural commands. It added a lot of clarity for us to separate mutation/command from queries. Because for the query part we had to use the dataloader lib, which has a very specific way of working.
This is the protocol used to dispatch the commands :
defprotocol KairosCommand do
@moduledoc """
KairosCommand is the modules in which the usecase / commands that define your applications
actions are.
All Commands should implement this protocol to ease the call and the formatting of the parameters
"""
alias KairosCommand.Context
@type result :: term
@type reason :: term
@doc "modify the builder data with the opts (keyword list of the params)"
@spec run(KairosCommand.t(), Context.t()) ::
{:ok, result}
| {:error, :forbidden}
| {:error, :unauthorized}
| {:error, {:validation_error, [KairosDomain.ValidationError.t()]}}
| {:error, {:impossible_action, reason}}
| {:error, {:argument_missing, missing_argument :: atom}}
| {:error, {:resource_not_found, [name: atom, id: String.t()]}}
| {:error, term}
def run(command_data, command_context)
end
And here is the command definition (one of the most simple command we have) and the protocol implementation for the command handler.
defmodule KairosCommand.DeleteOneShift do
alias KairosCommand.DeleteOneShift
@type uuid() :: String.t()
@type t() :: %DeleteOneShift{id: uuid() | nil}
defstruct [
:id
]
end
defimpl KairosCommand, for: KairosCommand.DeleteOneShift do
alias KairosCommand.Policy
alias KairosCommand.Context
alias KairosCommand.DeleteOneShift
alias KairosCommand.Ports.ShiftAggregateRepository
@spec run(DeleteOneShift.t(), Context.t()) ::
{:ok, :deleted}
| {:error, :forbidden}
| {:error, :unauthorized}
| {:error, {:argument_missing, :id}}
| {:error, {:resource_not_found, [name: atom, id: String.t()]}}
| {:error, term}
def run(%DeleteOneShift{id: shift_id}, ctx) do
data = %{shift_id: shift_id}
data
|> Chain.new()
|> Chain.next(&verify_is_authorized(&1, ctx))
|> Chain.next(&load_shift_aggregate/1)
|> Chain.next(&ShiftAggregateRepository.delete(&1.shift_aggregate))
|> Chain.run()
end
... (private functions are missing for clarity)
end
The mutation resolver creates the command and the context (with user permissions mainly) and calls KairosCommand.run(command, context)
Having one way to call commands and a standard response helped us mutualise and standardise the mutation resolver logic, and thus clarify the web/http part of the application. Quite a bit of plumbing in the end, but it happened over the course of 6 months, if we had to recreate that plumbing from scratch that would suck.
(Error handling, especially the form errors were automatically filled from the domain logic, translated and filled in the react form. Quite satisfying. Quite a bit of work for something Rails or Phoenix give for free… But Ecto.Changesets caused quite a bit of headaches so happy it ended up only in the infra, and business rules validations are free from it)
Apemb
Oh well, that one was not easy…
The way we did it was to use a DDD tactical design pattern : the aggregate. Long story short, it is (as the name suggests) an aggregate of multiple structs, that actually “change as one”. There is quite a bit to the story, but I feel it is not the space to discuss that, and I am not really an expert on DDD. I could find links for you to look into DDD - I wrote down two books at the end.
The interesting part is that this big struct, the aggregate, is part of the domain, and is designed to represent one business transaction that is either fully successful or a complete failure. A business transaction is one aggregate change. When given to save to the aggregateRepository module, that aggregate is transformed into a changeset spawning multiple tables. That is the way we made it work. No Ecto leakage outside the Infra module, the transaction is that changeset.
So the problem in the infrastructure is solved by a more complex design of the domain struct, and as they are quite complex and do not look much like what is the database, have adapters that do have a bit of logic - transforming the tables into the aggregate is easy, transforming the aggregate into a changeset that is not fun… I think we would do a different and much simpler database schema if we had to do it again.
Those aggregates are also the reason why the query part of the application came to life, as manipulating those big aggregates for a small read was increasingly painful performance wise. (Some of our aggregates needed a join query of more than 10 different tables…)
Yet again, I think it was worth it, because following the DDD teachings is about more than technicalities, and more about a better way to communicate with the business persons the team works with. The aggregate reacts like the concept the stakeholder say its business concept does. It has the same name, works the same way. The price you have to pay is plumbing, and in our case a read part of the application.
I encourage you to read Eric Evans’ “Domain-Driven Design: Tackling Complexity in the Heart of Software” Book or the shorter “Domain Driven Design Distilled” by Vaughn Vernon. (I read the short one, and I should do it again because I feel I missed some parts). Either one will help you understand the philosophy behind the aggregate 
Sorry to answer your question by a concept that takes a book to understand, if I knew how to explain it better I would, but really I am not yet able to summarise DDD in one blog post ^^. Maybe in a year or two.
LostKobrakai
I feel like this might actually make it harder than easier, because there’s hardly any business logic to speak of. There’s “CLI App” – which is the bad outside world – and “http requests” – which is the bad outside world – and nothing in between worth noting by the description you gave.
Try answering the question what your app does on it’s own without the outside world.
For a different example: a calculator. There’s the input of the calculation to be done (maybe string based), then the business logic of parsing the input into an AST of calcuations to be done and then the application of the calculations, which give you the result. This internal result is then converted back to a CLI based visualisation.
For something like this the boundaries are much more clear. The functional core is the parsing to ast, and the calculation itself, while the imperative shell is the CLI stuff of retrieving argv and turning the result into something you can print to the CLI (stdout), possibly adding CLI specific coloring tags and such.








