Hexagonal architecture in elixir

Yes our ports return only KairosDomain plain structs. Structs that are some quite similar, some loosely and some quite different from the schema.

All the reste of the functions inside the command modules and domain modules do as if saving those struct was easy, use the correct port, and all is well.

The structs that end with Table are Ecto Structs, those with Entities are Domain Struct. (Our naming is like, Entites are readonly struct, Aggregates are the state-modifier structs)

# From KairosInfra.RecurringShiftEntityAdapter module

  alias KairosDomain.RecurringShiftEntity
  alias KairosInfra.RecurringShiftTable

  def from_recurring_shift_table(%RecurringShiftTable{} = recurring_shift_table) do
    %RecurringShiftEntity{
      id: recurring_shift_table.id,
      last_shift_generation_date: recurring_shift_table.last_shift_generation_date,
      timezone: recurring_shift_table.shift_service_request.warehouse.timezone,
      recurrence_pattern: adapt_to_recurrence_pattern(recurring_shift_table),
      recurring_template: adapt_to_recurring_template(recurring_shift_table)
    }
  end
# from KairosInfra.RecurringShiftEntityRepository

  def get(id) do
    result =
      RecurringShiftTable
      |> where(id: ^id)
      |> preload(
        shift_service_request: [warehouse: []],
        recurring_provider_assignments: []
      )
      |> Kairos.Repo.one()

    case result do
      nil ->
        {:error, {:resource_not_found, [name: :recurring_shift_entity, id: id]}}

      recurring_shift_table ->
        {:ok, RecurringShiftEntityAdapter.from_recurring_shift_table(recurring_shift_table)}
    end
  end

Those are one example with a loosely similar domain struct from ecto struct. Yes there is an adapter, yes the repository is not a plain Ecto function, but we gain a better error message, and writing the whole thing is like 5 min testing included. The longest part is creating the base data for the test factories. The rest is copy paste.

Why we did that on that particular struct, is more about consistency. We needed Ecto separation for one, we decided that it would be the way for all. (But as we refactored stuffed as we needed, I guess some stuff will never migrate as they never will be modified again…)

Is that needed for every project, no indeed, I do not think so either. In the startup I am working with right now, we have three different backends, and I see a moderate benefit in changing the architecture for one part of one of the three. What I am hopping is extracting that part into its separate app and going hexagonal on that one. And that isn’t very high on my to-do list :wink:

Hexagonal and clean archi are a nice clean way of organising code, extensible and easily maintainable, but expensive to migrate and in my opinion, useful only on a long and big project, or for complex business critical applications.