Fl4m3Ph03n1x

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

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

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 :slight_smile:

Apemb

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

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 :slight_smile:

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

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.

Where Next?

Popular in Questions Top

JDanielMartinez
Hi! May someone helps me, please! I have two apps into an umbrella project: the first one is Database, which manages queries, and the se...
New
dotdotdotPaul
Okay, I'm having a heck of a time trying to figure out how to best handle the validation of belongs_to associations in Ecto. I'm sure I'...
New
LegitStack
I’m hoping you guys can give me some general advice and perhaps code examples if you’re feeling up to it. I’m very interested in Elixir,...
New
tduccuong
Hi, is there any work on GUI with Elixir, that is similar to Electron/Javascript? My idea is to bundle Phoenix and BEAM into a single se...
New
fireproofsocks
I’m working on defining a simple Ecto schema for a table (in PostGres), but I don’t see where I can define a column as NOT NULL. Conside...
New
hariharasudhan94
I would like to know what is the best IDE for elixir development?
New
mathew4509
I have a list say x = ["23gh", "56kh", "97mh"] I would like to pass each element to Val in each iteration. Say, in iteration 1 -------...
New
LegitStack
I’m trying to make a websocket server in Phoenix or raw Elixir. I heard about gun, I think I could use cowboy, but since I’m not that sma...
New
Patoshizzle
After calling mix ecto.create I get this error: 17:00:32.162 [error] GenServer #PID&lt;0.412.0&gt; terminating ** (Postgrex.Error) FATAL...
New
9mm
I am constructing a JSON object (map) and I need to conditionally set a field. I’m trying to write proper elixir-way code… and I’m at a l...
New

Other popular topics Top

yurko
Here are few pieces of (common) Linux knowledge that we use for reasonably small one server apps. We use Ubuntu but this should work for ...
New
shahryarjb
Hello, I get Persian date from my client and convert it to normal calendar like this: def jalali_string_to_miladi_english_number(persi...
New
srinivasu
How to handle excepions in elixir? Suppose i have A, B, C ,D, E modules. and each module has get() function. A.get() method will call th...
New
vonH
In asking this question I am more interested about the expressiveness of the language itself and less concerned about the availability of...
New
_russellb
I want to try my hand at web scraping. What tools/libraries do I need to use. I’m hoping to turn this into something professional so don’...
New
nsuchy
Hi. I’ve noticed that Windows Powershell has it’s own IEX command and you cannot access Elixir’s IEX due to the conflict. This isn’t a cr...
New
chensan
I have a User schema with a :from_id field set to type :string: defmodule TweetBot.Repo.Migrations.CreateUsers do use Ecto.Migration ...
New
johnnyicon
Hi all, I've just started learning Elixir and Phoenix Framework, so please pardon my n00bness at this stage. I'm trying to use Postg...
New
belgoros
I’m not a pro in using Regex and can’t figure out why the following behaviour happens, especially if we take into account the difference ...
New
Qqwy
Original source of discussion: This topic on the Pragmatic Programmers' Functional Web Development with Elixir, OTP, and Phoenix forum. ...
New

We're in Beta

About us Mission Statement