Four questions about cross-context relations in Phoenix.

Greetings! I’m moving my first steps with Phoenix and I’m having some troubles understanding cross-context relations. I’ve read many discussions about it, the official documentation and articles, but I still think I’m missing part of it.

So sorry for the really huge post.

I’ve found various ways to deal with cross-context relations.

A) The most elegant way seems to be (according to my interpretation of How to determine contexts with Phoenix 1.3 ) to keep contexts completely separated. Entities from one context should refer to entities in another context only via public APIs. Relations should be expressed by simple integer fields instead of “belong_to”, or “has_any”.

B) Another approach I found keeps context separated as in A) but uses foreign keys at database level to ensure integrity.

Question 1) Which are the advantages of B over A ? I’d say that once you have linked two databases tables contexts are not independent anymore.

C) A third way can be found in the official context documentations ( https://hexdocs.pm/phoenix/contexts.html ) which uses both a foreign key and a “belongs_to” relation between different contexts.

Context Accounts has two schema: “users” and “credentials”; there is a TWO WAYS IN-CONTEXT relation:

  • users -> has_one -> credentials
  • credentials -> belongs_to -> users

Context CMS has two schema: “pages” and “authors”; “authors” has a ONE WAY CROSS-CONTEXT relation with Accounts.users:

  • CMS.authors belong_to Accounts.User
  • BUT “users” misses the reverse “has_one” relation

Question 2) Is it better to avoid TWO WAYS CROSS-CONTEXT relations or is it something one choose case by case? E.g. if we need to follow the relation in both ways or not.

Question 3) Is it true my understanding that option C is a sort of compromise between a monolithic project and full context separation? I say this because it doesn’t really separate contexts due to the “belongs_to” relations but still uses explicit interfaces between contexts.

Now, a slightly more complex example.

Let’s start from the blog project described in the official documentation ( https://hexdocs.pm/phoenix/contexts.html ) and summarized above as point C).

Let’s say we decide to add to this project the possibility for users to upload videos and publish code (not a very realistic example I know…). I’d be tempted to create additional Contexts videos and code:

  • Accounts
  • CMS
  • Video
  • Code

Let’s also say that blog posts, videos and software projects can have multiple authors.

Now, I really can’t see how to keep context separated: everything is strictly tied to everything else: users can have many videos, blog posts, software projects.

Let’s add a comment system on the top pf it: regular users (not necessary authors) can comment everything published. This is another feature that span all contexts.

I don’t think we can handle all these many to many relations only with integer fields instead of cross-context “has_many” relations, so here comes my fourth question:

Question 4) Is my choice of contexts poor? Or this example require a monolithic structure and there is nothing we can do to really isolate parts of it?

Thanks to everybody who took the time to read all this!

8 Likes

I think this is the crux of your problem - I suspect that you are not separating the intent from the implementation.

  • Users don’t actually own Blog Posts - Authors do.
  • Users only exist for authentication purposes.
  • Authors deal with Blog Posts

Now granted there is a rule that Authors have to be authenticated Users - but that’s it. So at certain times the CMS will need to verify that an author is an authenticated user and for that purpose the user_id is stored with the Author so that the CMS context can check with the Account context whether the User is still authenticated/valid.

It’s an implementation detail that both contexts store their data in the same database (making a foreign key relationship possible). If the contexts were in separate services the CMS may “suspend Authors” when an Account check fails or it may even subscribe to “User Change Events” from the Account service.

In any case it is the CMS that depends on Accounts:

  • CMS uses Accounts for authentication checks
  • CMS may (conceptually) subscribe to "User Change Event"s from Accounts to alter the status of any Authors it manages.
  • Users may own their credentials but nothing else; Authors own/share Blog Posts; Contributors own/share Projects, etc.
7 Likes

Well, its a coupling that’s less subject to abuse than putting in your application code, but you’re right, its still a coupling.

You can generally evaluate it on a case-by-case basis, but my feelings are that if a cross-context association is bad, reciprocal associations are worse.

Take a step back and ask why you need that association in the first place. Is it really necessary to link the two artifacts? In the scenario you provide, a User is distinct from what I imagine to be more like a profile record in the Author. Why is it necessary to link the authors so tightly with the authentication mechanism, which is really what the user is for? Is it not enough to merely tag it with an ID or better yet, use a join schema that just contains user_id and author_id fields?

The reason associations are useful in the first place is to make it easy to crawl your object graph in code without having to explicitly query for each association. If you’re really feeling the pain of not having that in a given context, its a sign that you’re not keeping clean boundaries.

Yeah, I think that’s a fair statement. I kind of disagree with the premise in the documentation—I don’t recommend cross-context sharing that way.

Yeah, you can see that once you start chipping away at a boundary, things start leaking all over the place and you’ve just created a bit of a mess for yourself.

So yeah, this is kind of a simple example to really examine how you discover boundaries between different parts of your system. I probably would not go through the trouble of doing a deep domain dive on this subject matter. However, if you’re going to do it, you’ve got to shift your thinking a bit about it. Boundaries are not just about data. They delineate behavior and function before all else.

When I look at the example you describe, I see the boundaries a little differently (and probably I see some additional questions that you may not have alluded to):

  • Authentication - How do I log in?
  • Authorization - What can I see? What am I allowed to do with what I can see?
  • Content - What can I create? Videos and code both fall into this context.
  • Comments - How do I leave feedback? What can I leave feedback on? Is the mechanism of giving feedback different depending on the type of whatever I’m leaving it with?
  • Public profile What do I tell the world about myself? What if I want to have multiple profiles?

Comments are a particularly interesting case, in that a comment system may well be useful in many different situations. This is a good reason to use globally unique identifiers—all you have to do that way is tell the commenting system that you’re leaving a comment on some uuid, and the interpretation of that key is opaque to the comment system. You don’t have to have a different set of comments for code vs videos, and you don’t need to be able to get back to the code or videos from the comments. The interface becomes very simple.

I hope that gives you some ideas on how to shift your thinking in a way that seems more useful to you.

5 Likes

Thank you both for your answers! I think I see things clearer now.

Just one more questions; if one decides to follow the example in the documentation and use a cross-context relation:

Context Accounts:

  • User

Context CMS:

  • Post
  • Author (belongs_to Accounts.User)

Context Video:

  • Movie
  • Director (belongs_to Accounts.User)

Context Code:

  • Software project
  • Developer (belongs_to Accounts.User)

And then adds the comment/voting system as:

Context Feedback

  • Comment
  • CommentPost (relation between comments and posts: belong_to Comment and belongs_to CMS.Post)
  • CommentVideo (relation between comments and videos: belong_to Comment and belongs_to Video.Movie)

Is it true that it doesn’t reduce separation with respect to the original documentation example? What I mean is: we add cross-context relations from the new Feedback context (just like the original example uses them between CMS and Author) but CMS, Video and Code contexts are still separated.

Notes:

  1. I’ll try to think about the alterante boundaries suggested, I kept the old approach in this post since it has already been explained;
  2. I really think I am unconsciously reluctant to give away explicit relations between entities due to years of different habit with other frameworks so I’ll try to work on this but I still would like to understand better this case with cross-context relations, even if I’ll try to avoid it.

What’s worked for me for the last year (over multiple projects) is just having a single context called app. I’ve tried structuring my code every way suggested since the idea of contexts came about and I’m fully convinced that basically no web apps need this convoluted solution with multiple contexts.

So what I do is have an umbrella project with two apps: core and web. A simple example might be something like below.

Core:
lib
- core
- - app
- - - app.ex
- - data
- - - repo.ex
- - - schemas
- - - - user.ex
- - - - post.ex
- - - - comment.ex

The web project is just a regular phoenix app that only calls the functions in the files under app. I usually start with just app/app.ex and extract code into separate files as needed. E.g. you start with basic CRUD for users in app.ex but eventually they’ll need the ability to login/logout so you move all user related functions into app/user.ex.

This is probably the minimum separation that gets you the most value. It feels kind of freeing just having a list of schemas dumped into a single folder so you can just have relations the way you imagine they should work instead of wondering about crossing imaginary boundaries.

2 Likes

Just my perspective™:

For me, from a conceptual Comments context perspective there is no need for the Comments context to depend on either the Post or Movie context. There are things out there that want to be commented on but there is no reason for Comments to care what that is. It does care about keeping related comments together - so there is probably something like a comments_container that all related comments are associated with.

So in the simplest case Post and Movie context would have to handle comments_container_ids if they wish that the Comment context would handle comments for some of their posts or movies. Higher level contexts could be used to decouple them from the Comments context if that was necessary:

  • Post Comments context which depends on Posts and Comments
  • Movie Comments context which depends on Movies and Comments

Is it true that it doesn’t reduce separation with respect to the original documentation example?

The coupling that your are seeing comes primarily from the implementation choice of hosting the contexts on the same database. One context “accessing” another context’s table is a violation of that context’s autonomy - but it is also a trade-off to simply utilize that they are “living” in the same database so you don’t have to actually implement all that logic to enforce full separation right now. But there is the constant danger of simply devolving into an “integration database”.


Personally I find learning about contexts in this manner to be more difficult than if they were actually physically separated/distributed (which causes more work). The advantage of maintaining the boundaries inside a monolith is that it is easier to change the conceptual boundaries between contexts if they were suboptimal in the first place (which they often are). The disadvantage is that maintaining the established conceptual boundaries is an exercise in discipline and will because there usually is an “easier” way to deliver the next feature faster by disregarding the existing conceptual boundaries.

With that in mind I can see Dave Thomas’s point. The primary concern behind “Phoenix is not your application” should be not about contexts but about your application being a separate OTP application (from the Phoenix framework). Then when it gets to the point that your application needs to be broken into separate contexts, you can break out a separate OTP application, like a “User Profiles application” (and not necessarily as part of an umbrella project).

3 Likes

Thank you everybody for your invaluable inputs, I’ll start a relatively small pet project to put them in practice.

Very interesting discussion, I just read it all. I don’t know how I missed it before.

1 Like