sasa1977 / boundary

Manage and restrain cross-module dependencies in Elixir projects
MIT License
818 stars 21 forks source link

Handling circular dependency of Ecto schemas #62

Closed joecorkerton closed 9 months ago

joecorkerton commented 9 months ago

This isn't an issue so much as a situation I'm unsure how to setup boundary for. If this isn't the right place to post this, please let me know and I'll move it somewhere else.

We have some Ecto schemas defined for our database tables. We have 2 tables that are connected, with a foreign key association between them (so table B has a field A_id, that links to the IDs in table A).

We've set up our ecto schemas like this:

A.ex

defmodule A do
...

schema "A" do
    ...
    has_one :b, B
end

B.ex

defmodule B do

schema "B" do
    ...
    belongs_to :a, A
end

This obviously produces a circular dependency when I try to setup boundary here. This seems like valid schema code that someone might want to write though? So I am unsure how Boundary should be setup / code should be refactored in this situation?

Any advice is appreciated!

sasa1977 commented 9 months ago

In small-to-medium projects my preferred approach is to put schemas under the same namespace, such as MySystem.Schema or MySystem.Model (lately I prefer the latter, because I think that the name Model better conveys the main purpose). Then, you can have circular references between schemas. An added benefit of this approach is that you don't need to use plural names in modules to disambiguate, i.e. there's no need for e.g. Users and User (or Users.User, which I often see and personally find ugly).

If the domain is larger and you want to split it, then you need to decide who should depend on whom. For example, in a shopping system we could have the namespace MySystem.Account which is responsible for account management, and MySystem.Shop which is responsible for the shopping part, such as inventory, orders, etc. In this case, Shop will need to depend on Account, but the inverse dependency should not exist. Hence, Shop.Model.Order would have belongs_to :user, Account.Model.User, while Account.Model.User wouldn't specify has_many :orders.

In the most elaborate approach, something along the lines of hexagonal/onion/clean architecture, we'd aim for a pure domain layer which is completely decoupled from technical concerns such as persistence. In this case the model would consist of plain structs (not Ecto schemas). Ecto schemas would still be used to exchange data between db and Elixir. But as soon as the schema is loaded, the data would be transferred to a plain model instance. In such case, all Ecto schemas would again reside in the same namespace, for example MySystem.Persistence, and so circular deps would work fine.

joecorkerton commented 9 months ago

@sasa1977 thanks for the response.

I think we're running into an issue because we have a context ContextA where we want to have one schema for each DB table, and at the same time define 'sub-contexts' (e.g. we have ContextA.Foo, ContextA.Foo.Private and ContextA.Bar, we want to define sub-boundaries such that ContextA.Bar cannot access ContextA.Foo.Private, but it can access the shared schemas ContextA.SchemaOne ContextA.SchemaTwo etc, which themselves have a circular dependency).

We've gone for option 1 here (define everything under Entities module namespace i.e. ContextA.Entities.SchemaOne etc). It's not perfect but seems to be good enough for our use case.

Thanks!