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
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.