Best way to divide into contexts?

I was wondering if you have any advice regarding a small issue I have run into.

The users of my web app will be able to create two types of published content: A and B. These have their own separate database tables and structs. However, not all content is published. Some content items are drafts that can be published later. A and B have their own separate drafts: a and b, respectively. Both a and b have their own database tables and structs.

Currently, the front-end mostly fetches one of the four content types: A, B, a or b. But on some occasions it fetches a and b together from the database (e.g. “get me the 10 most recent drafts”). Or A and B together (“get me all publications”).

Until now I have had four contexts: A, B, a and b. This was a temporary solution. My first instinct was to create a “drafts” and a “publications” context, since this corresponds with the current front-end. However, the requirements for the front-end might change. It might happen for example that users will be able to fetch A and a together. Or B and b. Currently, that seems unlikely. But it is definitely a possibility.

How can I best divide my contexts? Optimize for the current front end? Put everything in one context, anticipating possible change? Or some other options? And most importantly, why?

The amount of actions per context (so 4 separate contexts) currently ranges from 7 to 14 (“create”, “list_all”, “publish”, “like”, “pin”, etc.).

Edit: One thought I forgot to mention. If necessary I can fetch data from two contexts and stitch them together after the database fetch. So two database queries, stitched together outside a context. But I reckon it is better to capture as much of the business logic inside contexts and let databases handle merging data??

The nice thing about DDD is that as the business requirements change your contexts may as well. The names of all your structs and your modules should be open to change so you should implement it how it makes sense now and when the requirements change you can rename your modules or add a new schema to reflect an alternative view of your table.

There’s no real rules here as with every software design pattern so it mostly comes down to feeling and iteration to get it right.

To me it feels like a single domain Publications with Publications.draft_content Publications.publish_content but I don’t have the overall context of your app to make that call. So go with your gut.

6 Likes

Just organize it how it makes sense to you (or your team in its whole, if such a consensus is possible).

Dev tooling is improving every year, even now you can go quite far with refactoring Elixir code with Emacs, VIM and VSCode.

4 Likes

I wasn’t sure if the refactoring-whenever route was considered bad practice or not. Considering the time it takes to refactor, that is. I have been doing any refactoring of this kind manually. So that might something for me to look into.

If you’re just looking for ideas, one thing you could think about is just having contexts As and Bs which contain A and a, and B and b respectively. I’ve done it this way with a Blog context that manages Draft and Post. Blog creates drafts, and publishes and unpublishes posts. Actions like “like” and “pin” could be potentially moved into their own “service” contexts. IE, extract the functionality as opposed to adding behaviour as you would in OO (although I’ve totally seen that in OO too). This way other things can easily become pinnable and likeable.
Again, just ideas, it’s by no means “correct” (or even good? :rofl:)

I think it’s natural for your design to change as your domain knowledge grows. Especially in the early stages when there are fewer entities the sakes are low!

3 Likes

Forget about the front end. Tables that are related through foreign key should be in the same context. Unrelated tables should be in separate contexts.

2 Likes

Do you mean your schemas and the associations you expose on those? For example: Accounts has a User with no associations, Blog has an Author with Post associations

Database tables and foreign keys have nothing to do with your contexts or your schemas.

Maybe there is another way to do it, but as far as I know, Ecto schema associations are backed by foreign keys on tables:

https://hexdocs.pm/ecto/Ecto.Schema.html#belongs_to/3

I think they mean that just because two things are related in the db that they belong in the same context. I think this is true though it depends on how DDD you want to go. Like, a Blog.Post could belong to a Accounts.User, and I think that’s fine in a lot of cases. If you wanted to be a little more DDD about it then you could say that a Blog.Post belongs to a Blog.Author where Blog.Author’s source is the users table defining only the fields in the schema that are important to it being an author. The latter way is cleaner but I don’t think the first way is wrong, especially in smaller apps. It depends on how much cruft your users table accumulates. Currently in an app I have a Users context with a User in it that is separate from the Accounts.User (though pointing to the same table). Basically, Users.User is a successfully logged in user and that’s what the various contexts interact with. I feel this might change as my app grows but for now it serves as a means to keep Accounts only about authentication.

3 Likes

Exactly, just because the foreign key exists in your database doesn’t mean you have to model it that way in your schemas which are just interpretations of that data. I tend to normalise data and go very DDD with many structs representing the same table data. I’ll make use of the :source option on the struct’s fields to create more relevant field names for those specific struct types.

E.g: Password and Identity both use the “credentials” table with the column “value”, but the sourced column is referenced as hashed_password and token respectively. A User will then has_one :password and has_many :identities. Then Password might be in the Access sub-context and Identity in the Identities sub-context.

How we store data and how we read it have very different best practices and we can achieve both with such design.

In this case, I would skip the foreign key constraints from Accounts.User to Blog.Post. There is no much value to enforce this rule, and there are scenarios where you want to delete the user but not the post:

  • A user decided to delete his profile
  • The user’s posts are still referenced by other posts

In the above case, the posts would be regarded as posted by an anonymous user.

The solution to the problem you suggest is to change the association constraint to nullify on the database rather than delete_all.

Lets say in this theoretical case the client has requested “The author’s name and a link to their profile must appear at the head of the blog post” so you have to model an association here in your Blog context.

With on delete set null you pay the cost of foreign key, but do not get all the benefit from the referential integrity. You have to handle the null case in the code, which is not much different from handing the case where an corresponding Account is not found.

If I need on delete cascade or on delete restrict constraint then I will model the association in Ecto. My point is: foreign key constraint is expensive, so if I am paying for it, I want to have the full benefit from it.

1 Like

Interesting—so you were literally talking about foreign key constraints whereas I thought you were speaking more generally about relationships. This is something I never dove into, mainly as I haven’t spent much time working on massive applications. I’ve found some interesting discussion on this stack exchange post and apparently GitHub doesn’t use them at all! I’ve been bitten by not having them in the past so now I just always add them but wasn’t aware there was even contention. So that’s cool to know, gonna read more! I have yet to understand when to use a mixed approached so in turn don’t understand how you would use fkeys as a guide as to organize Phoenix contexts. I personally feel a little uncomfortable, at least in my code bases, about dangling references, although it would make the UI of something I’m currently working on much easier to implement :thinking: Anyway, will keep reading up on it.

Don’t do the mistake of “Google uses this super-specific list of tech stacks so we should all use it”. GitHub optimizes for quicker and cheaper DB queries because they don’t want their DB bill to jump to the skies and because they want people’s UI to react faster. That’s all there is to it really.

I have refused working in a project with super-quirky DB policy (including not using foreign keys) because I have done this dance before (hobby + professional projects long ago): they basically move the costs of the proper DB practices to their already-paid costs of engineering salaries (to use the managers’ lingo). Translation: you end up reinventing the DB inside your application’s code.

Nah, I am good.

4 Likes

Oh don’t worry, I wouldn’t be here if I made decisions based on what MAANG does :smiley: I’m still skeptical and just pointing out github as it was surprising to me. It’s more that I’ve been blindly using fkeys without questioning it and there is one backoffice part of my current app I was feeling would be easier if I didn’t have fkeys but in my mind I was like, “but I must!”

1 Like

Yep, I agree it’s useful for us to be made aware of our blind spots. Helps us hone our skill to pick the right tool for the job.

1 Like