Context Questions

Just had a couple questions around contexts I was hoping to get some general answers on. I know its really all opinion based but would love to hear some of those.

  1. Is it appropriate for my web controllers (used for full-stack phoenix app) and api controllers to share a single Context? I assume it is but then I have also seen examples where people break a Users table into a bunch of different contexts for authentication, CRUD functions, etc… I know its all subjective but for this question lets say I only want to access my ‘users’ table in one context instead of abstracting it into multiple.

  2. If I did want to access my ‘users’ table in different contexts would I have to give the context generator the option of “–table users” at the end to ensure we are referencing the same table in our contexts? I assume this is the case but just want to make sure there is not magic chopping of the ‘contextName_’ preface to the table or something.

  3. Lets say I have 3 tables:
    Users - Will have the standard email and password
    Company - Will have the details of the company such as name, etc… (everything else in the app will belong_to the company)
    CompanyUser - Will be a join between company and user and also have permissions on it for what the user has access to in that company.

Summary: a user will have access to many companies via CompanyUsers and there will be permissions on the CompanyUser. Everything else in the app will inherit and be associated from the Company (belong to the company)

Can someone give me some ideas on how I would structure this with contexts? (I understand there are better, more efficient ways to do role based permissions, but for the sake of contexts I feel like this would be a good example)

Thanks!

Only if you that other context will also be modifying the user’s table. If it just needs the access provided by the User context, then you can use the modules and functions in the User context. If, however, there will be a separate set of schemas (and possibly migrations) in that new context that should also modify the User table, then yes … but then I would ask myself why that functionality is not in the User context.

Remember that you can easily use code across contexts … it’s just an organizational strategy. The modules, including the schemas, are available everywhere.

Personally, I would go with a context for Users and Companies, and then the rest I wouldn’t bother with any database stuff. You can create other contexts, if you wish, and just remove the database related files after gneration.

Thanks for the reply. So a use case for accessing a table from multiple contexts would be if I want to update certain attributes without risking modifying others on the User table correct?

I see two possibilities (there might be even more)

  1. You have a Users context that is the only one that defines a User schema and accesses the accompanied table through the Repo. All other contexts talk with Users to obtain information about a user, update some details. If information about a user is stored in another context’s data, the user is referenced by its user_id.
  2. You have multiple contexts that all access the users table through the Repo, but define their own schema with different (possibly partially overlapping) fields that can be accessed.

Contexts are a way to perform encapsulation, and therefore make your system more loosely coupled. So indeed, you decrease the ‘risk modifying other attributes’ when you split things off into multiple contexts. If you want to split the table, or reference two parts of different fields in the same table, depends on your situation.

… I cannot actually tell you when to use which one. I have a personal preference for (1) for some reason, but I have no clear arguments for you. Maybe someone else can append to this.

Personally, I would see multiple contexts using the same table as a red flag. I’m basing this judgement on microservices design guidelines:

  • Each service is it’s own context.
  • Each service has has full autonomy over it’s own database.

So as already indicated the easiest way to deal with that is to simply assign the table to one context or the other.

However what seems to be often overlooked is that a service (context) can depend on other services (contexts). So the other possibility is that the join table is part its own context. Now the tough pill to swallow is that this new context has to respect to the autonomy of the other contexts that it is based on - so it can’t access those contexts’ tables directly - e.g. conveniently through some Ecto query. Instead the primary contexts need to offer bulk retrieval capabilities - e.g. retrieve the info for this list of company IDs, retrieve the info for this list of costumer IDs. Then the new context can weave together the necessary information based on the join data that it alone owns.

Now this is the typical kind of tradeoff that often is made to optimize for maintainability - there is always that moment - “this would be so easy if I could just use an Ecto query” - but it’s the sequence of all those “easy” steps that ultimately lets an application devolve into a brittle, tightly coupled mess.

1 Like

I’ll take the simple choice of answer here. You have 2 entities plus 1 join table. Your app is not complex enough for contexts yet. Either don’t use them (yet) or pick a generic name like “MyApp”, or “CoreLogic”, etc. for a single context. Once that grows to a couple of hundred lines of code and/or you have trouble navigating that module’s source file, revisit and see if you can find different groups of behavior, pick names for them, and refactor the code into multiple contexts with those names.

5 Likes

100% - And Chris has been very clear about this:

Our generators on Phoenix are just learning tools

There was an opportunity to raise awareness that there are better ways - and the artifacts created by a generator are just one possible starting point.

The flip side is that the tolerance of inceptions without or with only a single context shouldn’t be seen as a license to revert back to the old ways (well actually everybody has the freedom to do exactly that - just don’t expect any sympathy when you are faced with the consequences of your actions). There has to be a commitment to constant vigilance to detect opportunities for improvement as early as possible and to exploit them through refactoring, even at the risk of delaying the implementation of new features. It’s an unavoidable effort in the pursuit of a sustainable and manageable level of technical debt.

3 Likes

In that situation, I would add API to the Users module which does this, and then use that API from wherever it is needed. That way, the Users data storage is entirely encapsulated by the Users module, and you can (more) easily reason about the things that may happen to the data itself by looking at just the Users module.

So let’s pretend that there is a postal address stored with a User … The User module might provide a associate_address(user, address) function which expects user to be either a numerical id or a %User{} struct, and address to be a map with the new data. That function would then know how to associate that address with that user. It may be stored in the users table (not the best idea, probably :slight_smile: ) or in another table with a 1:many relationship to the users table. That is hidden behind the API of the Users module, and so can change later if needed. That API can be used from wherever, and it means the actual data storage and structure can become entirely opaque (and therefore an implementation detail) to the entire rest of the app.

tl;dr → IMHO, keeping all actions related to a given type of data or topic in one module which provides a sane API to do the manipulation is a good approach.