Hey!
I was thinking about this problem recently, and I concluded that there is no “good” solution. However, there are different tradeoffs.
My problem with Phoenix Contexts, in general, is that they reuse schemas as application data. E.g. if you create a context via generator like this:
mix phx.gen.html Accounts User users name:string age:integer address:string
This command generates templates, view, controller, context and schema. The %User{} record is present in all those places. Let’s say that users are central in our application, and many other contexts use them somehow. All of those have different schemas that use some subset of those fields. E.g. SnailMailDelivery context uses only User.address.
Suddenly, there is a requirement that makes you split the address into street, number, city and zip code. You have to change the Accounts context and SnailMailDelivery context and every other context that used address field.
In your question, you asked if adding context-specific fields to Calendar is OK. I think it is perfectly OK. The problem is with data shared between contexts.
Another problem might be if, for some reason, you decide to refactor Inventory into a polymorphic association to use with Calendar. Suddenly, you have a sweeping change through almost the entire project. Any relation or column that you use in two contexts (maybe except ids) is a potential problem. I still think it is an OK solution because it is simple, and contexts give an excellent high-level overview of your application. It is one of the suggested solution in the official docs 1. Intro to Contexts — Phoenix v1.8.8 along with creating micro tables with 1:1 relations.
I believe the problem stems from the fact that the DB is just a big global variable, and no amount of structure can change that. The solutions I came across are:
- Use event sourcing and have separate storage for each context
That is a purist solution and requires lots of setup, so I’ve never tried it. SQL databases give me ACID, and from what I understand in Event Sourcing solutions, you have to work hard with sagas and what-not to get similar properties. (don’t quote me on that, I might not have studied it hard enough)
- Try to treat your database as an after-thought in your project
If I needed to summarise “Designing Elixir Systems with OTP” by Bruce and James, it would be “Code without a database and then add it later”. In the book, they go to great lengths to avoid touching database. When the project is ready and functional in memory, only then they make a separate OTP app with DB schemas and pull it as a “poncho” dependency. For me, it was overkill. I understand that database schemas tend to “spill” into your project resulting in hardcore refactors for a schema change. But that was a lot of config just to abstract the DB.
- Use Service/IO/Model (Service/DA/Core)
My favourite approach is explained in a talk by Rafał Studnicki https://www.youtube.com/watch?v=XGeK9q6yjsg He splits every contexts into three parts: pure “model” (in Dave’s book it is “functional core”), then “Data Access” layer with schemas, and in the service layer that uses the two.
The idea is that in the core you have pure Elixir data structure %User{} (not a schema). In DA, you have the schema, but you keep it private. E.g. Accounts.DA.get uses the schema internally but returns Accounts.Core.User record.
The downside is that instead of DRY, you now have to use WET (Write Everything Twice
). But now you can safely refactor schemas. If you refactor address into four separate fields, you need to refactor DA layer in all contexts that use the %User schema but only that.
I think this has the minimum overhead for maximum gain approach. Repeating schema once more is not a big deal. It is already repeated in form, schema, changeset and migration. Adding fifth place for making future refactors a little bit easier is worth it in my opinion.
Rafał also talks about using Dialyzer to help you keep structs private to contexts. Highly recommend watching it ![]()






















