Earlier today I asked a question in the public Slack channel and it resulted in hours of conversation about how to break up a context, and multiple people had different opinions.
This makes me think that most people really enjoy the idea of having contexts around, but the documentation, generators and the Programming Phoenix 1.4 book do not make it clear on how to use them beyond very tiny examples.
As of Phoenix v1.4.8 the documentation and generators produce code like this:
$ mix phx.gen.context Accounts Credential credentials \ email:string:unique user_id:references:users * creating lib/hello/accounts/credential.ex * creating priv/repo/migrations/20170629180555_create_credentials.exs * injecting lib/hello/accounts.ex * injecting test/hello/accounts_test.exs
The basic idea is:
- You have a single entry point into your context as
- You have a
lib/hello/accounts/directory to hold any files that your context needs.
There’s also the example application in Programming Phoenix 1.4 that follows this style.
Problem 1: A large context ends up having 1 public module with too many functions
The problem is, what happens when you have a context that has more than a small handful of files and only a few functions that you want to publicly expose?
Let me give you a real example based on an application I’m developing as we speak. Which is a course hosting platform.
This was a 2nd pass attempt of coming up with a
courses context using the rules provided by the documentation, generators and book written by the creators of Elixir and Phoenix.
courses/ course.ex landing_page.ex overview.ex package.ex section.ex lessons/ lesson.ex note.ex transcript.ex video.ex attachment.ex lesson_attachment.ex
From an admin’s POV, almost everything listed there would be CRUD’able and from a “student”'s POV (the person watching the course), all of those things are pretty tightly coupled from a business sense.
For example, a student would watch a course and then be presented with a table of contents with sections and lessons, and each lesson has videos, transcripts and attachments.
I’m not done coding the platform yet so I can’t give you an exact line count, but if you had a single
lib/hello/courses.ex file as the sole public interface for this context then I would bet you would end up with over 100 functions and thousands of lines of code. That makes it very difficult to reason about IMO, but that’s only problem number 1 with the above set up.
Problem 2: The
lib/hello directory gets overloaded with a grab bag of files
The second potential issue with the
lib/hello/courses.ex style is what happens when you have a bunch of different contexts? I’m barely into creating this project but I already have contexts for accounts, checkout, payments, enrollments, courses and questions. I’m for sure going to have many more things too (progress tracking, affiliate systems, and the list goes on).
What’s going to eventually happen is the
lib/hello/ directory is going to have Phoenix specific files like
repo.ex but now it’s also going to have 15 different context entry point modules and potentially other modules that are related to my project but aren’t contexts or Phoenix specific.
That makes looking into the
lib/hello/ directory a very crazy experience where you’re overloaded with many different types of modules and we’re back into a big mess that’s comparable (but not as bad) as the old “models” directory.
I’m already experiencing this problem with only 4 contexts that I have implemented when they are all mixed into the other modules in that directory.
A potential solution
After hours of discussion and input from others (special thanks to @LostKobrakai for having a really long convo and providing valuable input about my domain), we kind of decided on this:
lib/ hello/ accounts/ accounts.ex credential.ex
So the basic idea is, problem number 2 is immediately resolved because your context entry points are not exposed in
lib/hello/accounts.ex. Instead it’s in
lib/hello/accounts/accounts.ex (the public module is the same name as the directory).
Problem number 1 also goes away because you’re no longer limited to a single entry point for your context. For example, using the courses example from before, you could now end up with this instead:
courses/ courses.ex course.ex landing_page.ex overview.ex package.ex section.ex lessons/ lessons.ex lesson.ex note.ex transcript.ex video.ex attachment.ex lesson_attachment.ex
And it would be expected that
lib/hello/courses/lessons/lessons.ex are the public entry points since they have files that are named after the folders they are in.
As for naming conventions of the modules you could have
lib/hello/courses/courses.ex but non-public modules would have the full path, such as
So my proposition is to potentially update the context documentation to include something like this, and if a strong enough use case can be made, maybe even change the generators to use this style by default. Chris said discussions like this belong here, so that’s why I posted this here.
This style is already being used on other projects successfully. Even the hexpm website uses something similar where files with a plural inside of a context are public, which you can see here: https://github.com/hexpm/hexpm/tree/master/lib/hexpm/accounts.
What does everyone think? Is there a better way to split contexts up so they work for both small and large contexts? What are you currently doing in your non-toy examples?