earth10

earth10

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 - #72 by LostKobrakai ) 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 ( 1. Intro to Contexts — Phoenix v1.8.8 ) 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 ( 1. Intro to Contexts — Phoenix v1.8.8 ) 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!

Most Liked

peerreynders

peerreynders

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.
karmajunkie

karmajunkie

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.

peerreynders

peerreynders

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).

Last Post!

baldwindavid

baldwindavid

(post withdrawn by author, will be automatically deleted in 24 hours unless flagged)

Where Next?

Popular in Questions Top

minhajuddin
I have seen a lot of code which picks the first element from a list using Enum.at(0) instead of List.first. Is there a reason why people ...
New
hariharasudhan94
Lets say I have map like this fetching from my database %{"_id" => #BSON.ObjectId<58eb1a7a9ad169198c3dXXXX>, "email" => ...
New
lastday4you
I wanted to check elixir version in phoenix because i found that my elixir is 1.5 but when i use Enum.chunk_by it said the function is un...
New
Lily
In templates/appointment/index.html.eex: <%= for appointment <- @appointments do %> <tr> <td><%= appoi...
New
greenz1
I have a phoenix application from which a user can download multiple(5-6) files of size 1MB. I couldn’t find anything related to sending ...
New
aalberti333
As the title describes, I’m trying to run Enum.map() over a list of key/value pairs, where the value is a map. My data looks like this: ...
New
JorisKok
I have a server on AWS, and was running a load test using artillery. When looking at the Phoenix dashboard I see the Ports going to 100% ...
New

Other popular topics Top

minhajuddin
I have seen a lot of code which picks the first element from a list using Enum.at(0) instead of List.first. Is there a reason why people ...
New
New
chrismccord
Phoenix 1.4.0 released Phoenix 1.4 is out! This release ships with exciting new features, most notably with HTTP2 support, improved deve...
688 31494 112
New
gausby
I asked this very same question on twitter and got some interesting feedback, but I thought it would be a good question to ask here as we...
1207 40042 209
New
alice
Hey, Just curious what are the main benefits of Elixir compared to Clojure? When is Elixir more useful than Clojure and vice versa? Th...
New
Patoshizzle
After calling mix ecto.create I get this error: 17:00:32.162 [error] GenServer #PID<0.412.0> terminating ** (Postgrex.Error) FATAL...
New

We're in Beta

About us Mission Statement