How to organize Phoenix contexts with schemas/changesets

Please forgive the somewhat simplistic thread title. What I’m really asking is how do you all design your contexts with regard to schemas and changesets? And similarly how do you expose DB interactions via contexts?

Let me give you an example, many Phoenix projects will have the following context setup:

/accounts
  accounts.ex # Contains public interface for context and functions which use 'Repo'
  user.ex # Contains user schema and changesets

Now let’s suppose we add a new ‘WorkExperience’ association to a ‘User’. i.e. a User has_many WorkExperience’s, and each WorkExperience belongs_to a User.

Work experience code is rather complicated and not obviously accounts related so we add a new context like so:

accounts/
  accounts.ex # Contains public interface for context and functions which use 'Repo'
  user.ex # Contains user schema and changesets
work_experiences/
  work_experience.ex

We now add a form to edit a user’s WorkExperience, where should the business logic and changeset go for this?


The ‘easy’ choice would be as follows:

# accounts.ex
def set_work_experiences(%User{} = user, params) do
  user
  |> User.work_experiences_changeset(params)
  |> Repo.update()
end

But now we are adding non-account related functionality to the Accounts context. This also doesn’t scale as we add more complicated associations to the User (in our case the User schema is quite complicated).

Here’s one possible solution to this problem:

user/
   user.ex
accounts/
  accounts.ex
work_experiences/
  work_experience.ex

i.e. Don’t store the User schema in Accounts but separate it out into its own context. This context could contain the schema and functions to update the User table. We can then leave User changeset definitions to whichever context is most appropriate. e.g.

# work_experience.ex
def set_work_experiences(%User{} = user, params) do
  user
  |> work_experience_changeset(params)
  |> User.update()
end

Apologies for the lengthy post. Does anyone have any thoughts/experience with this? It’s hard to find info for how people are organizing contexts at scale. It’s very possible I’m missing something obvious.

Thanks in advance!

1 Like

There is this blog post Putting contexts in context from @michalmuskala.

Nothing forbids You to have …

accounts/
  accounts.ex # Contains public interface for context and functions which use 'Repo'
  user.ex # Contains user schema and changesets
work_experiences/
  work_experience.ex
  user.ex # Access the same db as accounts/user, RO, work_experiences associations

Thanks for the reply, and thanks for linking to the article. :slight_smile: I see your solution is mentioned as:

Some alternative approaches (that still prevent you from having cross-context associations) include: having schema in each context reading from the same table (each having access to mostly different fields),

The thought of having many User schemas all slightly different but referring to the same table feels a bit weird though. This may be simply because the concept is new to me, but I haven’t seen any open source projects do this and it isn’t very DRY.

What are the benefits of that solution over defining the schema once in its own context and defining changesets in whichever context is most appropriate? i.e.

user/
   user.ex
accounts/
  accounts.ex
work_experiences/
  work_experience.ex
1 Like

I actually think this is a good idea and I see them more as different views into the database. You can almost think of it as database views or stored procs (which are very common) and the reference the same table but for different contexts.

Instead of having one User module having to deal with all cases everywhere in your application you have a User specific view for a particular context only.

Having one User to me means lots of accidental coupling and complexity and I don’t think DRY in this case is that important. The both User objects may have different use cases, APIs and interactions therefore it makes more sense to keep them apart.

That said, I also struggle a bit with how to layout the code and things don’t come together until I have a very clear picture of the business domain. Up until then I go wrong many times. Luckily it is easy to refactor. :slight_smile:

1 Like