baldwindavid
Any downsides to using the same table for multiple contexts?
I’ve really enjoyed using contexts to organize and protect my business logic from the web app. I’m attempting to more fully decouple my contexts though and wanted to check with others to make sure I’m not going to down a bad path.
I’m building an app to manage shared office suite buildings. There are more contexts than this, but let’s focus on the following four:
Accounts - Users and Profiles, Authentication
CompanyMgmt - Companies can lease multiple spaces in our buildings and a company can have multiple users
Inventory - Locations, Offices, Conference Rooms, Spaces (like first floor), office types (salon, focus, coworking, etc.)
Calendar - Reservations - Members can reserve conference rooms
I’ve previously tried to keep these somewhat decoupled by allowing for cross-context belongs_to relationships, but no has_one/has_many. I’d like to remove all cross-context relationships.
Let’s take the “Calendar” context as an example since it is intertwined with a lot of other contexts.
There is a calendar for each of our building’s locations. Any company with the lease of a suite in that building gets 20/hrs/mo of free time to reserve space. If they have multiple leases, they’ll get more time.
Each location has about four conference rooms and depending upon the size, different rooms may count more against allotted monthly hours.
Companies have multiple users and the time reserved by the users is pooled together against the allotment.
All in all, there are the following cross-context dependencies in the “Calendar” context:
Inventory.Room - Rooms are pulled directly from the Inventory context. Reservations have a belongs_to room association.
Accounts.User - Reservations have a belongs_to user association.
Inventory.Location - Each location has a calendar. These locations are directly from the Inventory context.
CompanyMgmt.Company - There is no direct association between companies and reservations, but users have reservations and companies have users. I need to let users know how many hours their company has left and reporting on usage by company.
To fully realize the benefits of decoupled contexts, I originally thought that I would need to have a separate table for each of calendar users, rooms, locations, and companies. This would be much like the concept of an “author” record in a blog. This seemed like a ton of work. However, I realized that I can just create separate schemas for each of these entities in the Calendar context using the same table as the schemas in the other contexts. I can even have the exact same associations, only they now associate to the Calendar-specific schemas. This means that none of my queries (including preloads, joins, etc.) needed to change at all.
This allows me to remove a few calendar-specific attributes from other contexts.
This brings me to my questions:
- All in all, this was an extremely simple refactoring to the point that I’m wondering if it is so easy that I am missing issues this might cause me down the road. Are there problems this might cause me in the future?
- Is this the “go to” way to handle this sort of thing in cases where “all” of the records for a given table are relevant to a given context.
- I’ve previously tried to avoid throwing a bunch of extra columns on to a given table for different use cases. For example, if there were 50 different company settings for different scenarios (like calendar usage), I would probably break those into different tables to, if nothing else, avoid accidentally loading all of those settings up when grabbing a company record. However, different schemas ensure that only the specified attributes are ever selected. Thus, there doesn’t seem to be much of a downside to dumping a bunch of columns into one table. Any disagreements on that point?
Most Liked
sasajuric
You did a great and an extensive analysis in your post, but this is the part I disagree with. Those four places may already seem like a lot, at least to people just getting started or people writing smaller systems. Adding a fifth place (possibly even 6th if you want to add specs, which I believe you should) is going to make things quite tedious. To be clear, I don’t think this is a bad approach, but I feel the effort only becomes worth it in larger codebases.
The gist of the tradeoff is expressed here:
Adding fifth place for making future refactors a little bit easier
You seem to argue we should overcomplicate the design today to make some possible future change easy. This seems like a classical case of YAGNI. Martin Fowler did a great treatment of the topicin this article. The gist of it is:
a) You’re betting on one of the many possible futures. Chances are slim that you’re right.
b) Even if you’re correct, a lot of effort will be spent maintaining an overly complex design before the future change is actually needed.
For more discussion of the topic I also recommend Is Design Dead by the same author.
To answer the original question - I believe that it’s fine for multiple contexts to use the same tables. In my view, Phoenix contexts are not DDD bounded contexts. Bounded contexts are used in much larger domains, and my impression is that people typically use different databases or at least completely different db tables. That’s a lot of ceremony, and you need to have a large enough use-case to justify it. My impression from the original post is that the domain is nowhere near as large to justify such ceremony. As usual, there’s no one size that fits all scenarios.
When it comes to Ecto schemas, I think that it’s fine to use them across contexts. Schema is part of the context interface (after all, we return schemas to Web), so I see no reason why one context shouldn’t use schemas from another one. TBH, I don’t really think that schemas are owned by a particular context anyway. They are context-level entities, but not necessarily tied to some particular context.
However, I do agree with this:
I wouldn’t put it so strongly, but I do agree that a case of two contexts updating the same schema is a likely design issue, for example that things which belong together are separated. Which leads me to the following question:
I’m not completely sure I understood the problem, so let me paraphrase it. Let’s say that we must support the following scenarios:
- A user can update their own profile
- An admin can update anyone’s profile
The way I’d model this is via a single function, e.g. Profile.update(user_id, updater_id, data_to_update). Both admin UI and user UI would invoke this functionality. The domain rules related to profile would be encoded in the single place. When you want to understand how a profile can be updated this would be the single source of truth.
This would also mean that I’d have no admin context. Admin UI is a UI concern, and UI is just a view of the domain, but it’s not the domain itself. Both UI and domain are driven by the current requirements, but that doesn’t mean that the domain should map exactly to the way UI is organized.
tomekowal
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 ![]()
sasajuric
I used ids here mostly b/c I assume the client is the web tier which doesn’t have the user struct, only the id. If the client code has the struct, I’d make the API accept the struct (it should probably be done for the second updater, b/c web likely already obtained the user struct through auth).
This decision process could lead to some inconsistencies (some funs accept ids, other structs), but I’m personally not to worried about it. If it bothers you, you can always require the client to pass the struct, but then they’ll have to make two API calls, something like:
with {:ok, user} <- Profile.fetch(user_id), do: Profile.update(...)
My personal opinion is that a design which is simpler is more open to any possible future change. When that change arrives, if it doesn’t fit into the current design, the code should be refactored (basically - make the change easy, then make the easy change). This simplicity is obtained by making the design reflect the present, not some possible future.
So I like to split things based on what the current code does, not on what it might do. In practice, this means that I’ll actually combine some seemingly unrelated things in the same context module (i.e. I’ll avoid splitting things too early). Once the module grows large enough, I’ll have better insight to identify the distinct logical groups.
I recommend reading the Fowler articles I linked in my previous post, because they provide an excellent and an extensive treatment of this approach.
Popular in Questions
Other popular topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #deployment
- #library
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #channels
- #elixirconf
- #exunit
- #discussion
- #code-sync
- #javascript
- #podcasts
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixir-ls
- #phoenix_html
- #iex
- #blog-post
- #graphql
- #genstage
- #ai
- #websockets
- #supervisor
- #advent-of-code
- #elixirconf-us
- #distillery
- #processes
- #forms
- #api
- #metaprogramming
- #security
- #performance








