The recent changes around Phoenix 1.3, especially the introduction of contexts, seem to be confusing for many - I decided to write a bit more about my perspective on this, and hopefully, remove a bit of the confusion.
The initial draft of this post was created over a month ago. The recent ElixirConf was the push I needed to finally publish it.
Clear your mind
Before we go any further, I’d like to ask you, dear reader, to clear your mind. Forget about all you’ve heard, read, or saw that relates to Phoenix contexts. Clear your thoughts and open your mind.
Actually, for a moment, forget about Phoenix completely, and let’s start with the basics. We’re going to look at a regular elixir application first, and translate the patterns we discover there over to the contexts of Phoenix.
The structure of an application
We’ll take a look at an elixir application: ecto. We all know the main modules of Ecto, that user interacts with -
Schema. Are those the only modules? Hell, no! Ecto is, as of now, a whole lot of 70 modules. So what are all those extra modules doing, if you don’t use them? For example, there’s
Ecto.Repo.Supervisor responsible for managing the repo processes, or
Ecto.Repo.Queryable responsible for executing queries for functions like
preload. Behind each module you use every day in your application, there’s a set of implementation/collaboration modules that do the real work. The public interface for the end user, for the consumer, is well defined within one module, but the implementation is spread out over multiple private modules.
Another interesting example, we should look at, is how migrations are implemented. We all know there’s a
mix ecto.migrate task. But is all the code for migrations right there, in the task module -
Mix.Tasks.Ecto.Migrate? No! Ecto exposes a public interface for running migration in the
Ecto.Migrator module. The mix task is merely one of the consumers of this interface. Another consumer could be a custom release command, as described in the distillery guides. Behind the migratior module, there are also other collaborator modules - but we already know the pattern. What is important in this example, is that inside one application we have a module that exposes a public interface, and another module that is a consumer of this interface.
What about Phoenix contexts?
And here we come back to a Phoenix application, and the concept of contexts. A context is a module, with some implementation modules behind it, that exposes a public interface, the rest of your application can consume. Sounds familiar? That’s exactly how ecto (and a lot of Elixir applications) is structured!
The biggest mind shift here, in my opinion, is that you’re now implementing your application - your business logic, separate from the web interface. The web interface is merely one of the possible consumers of the API (modules and functions) exposed by your application.
Why is that important? Because you can have multiple consumers of those interfaces - HTML web app, REST API, GraphQL, mix tasks, real-time API through channels - it’s not important how your application is exposed to the outside world - you have a place to declare what constitutes your application, what operations it supports - the context modules. This allows you to easily build multiple interfaces, without duplicating the business logic - it was a common issue with earlier versions of Phoenix to have this kind of duplication between regular web API and channels.
That’s it - that’s all there is to “contexts” in a Phoenix application - modules and functions defining public interface of your application. Anything more, any more philosophy you’re going to attach to this is entirely your choice.
What about this DDD thingy?
Now, after we’ve hopefully understood the main objective of contexts, let’s quickly look at Domain Driven Design, and why it’s so often mentioned in the context of contexts 😅.
The goal of contexts is to encourage defining clean interfaces and clearly delineating where business logic should live. It so happens this is exactly the same general goal behind DDD. This means that the techniques described by DDD are one, rather formalised, way to achieve this goal, but definitely not the only one. It is the choice of your team if you’re going to give any more meaning to contexts if you’re going to adapt the DDD techniques.
Every project has it’s fair share of plain CRUD interfaces and a part of a complex domain. It might be a good idea to adapt the DDD rules in one part while keeping the plain CRUD part simple. The important thing is that it’s your choice - Phoenix does not impose anything here, does not attach any additional philosophy to contexts, beyond encouraging clean interfaces.
Exchanging data between contexts
I wanted to also touch at this - how do you exchange data between contexts. This often takes the form of a question “Can I have associations (such as
belongs_to) between contexts?”.
It’s natural to link to other contexts, but I’d say for the best “bang for your buck” (in terms of time you’re going to work on the project), it might be a good idea to keep the references to data in other contexts as opaque terms. So, for example, instead of having an association, you’d only store the id. You can still access the data using the public interface of the other context. This does not mean you shouldn’t have a foreign key reference on the database level to guarantee data consistency - but on the application level, I believe, that using clearly defined boundaries between contexts is beneficial.
This is actually something similar to the “Law of Demeter”- you should limit the knowledge of the components in your application. They shouldn’t be reaching too far into other components.
Some alternative approaches (that still prevent you from having cross-context associations) include: having schema in each context reading from the same table (each having access to mostly different fields), or having multiple tables that use the same primary key value (so you don’t have to keep a separate foreign key around). The baseline is: it’s completely fine to use functions from one context in another one - this makes the dependency explicit and should make any future changes much easier.
How many contexts should I have?
This is a tough question to answer in general - it hugely depends on your application and the domain you’re modelling. It might as well be your application will have just one context! There’s nothing wrong with that.
It might be actually more harmful to split contexts too thin, than not to split them. You can always come back and refactor extracting a context. A premature separation will slow down your development speed and will cause you issues.
Some warnings signs that should tell you when you’ve split something that should have been together are: a lot of relations between contexts, heavily using data structures from one context in another one, or a need to handle transactions across the contexts.
Now, my context modules are huge!
This is a fair concern. There are several techniques that you can use to limit the issue.
The first one is to just remove the functions! With the default of phoenix being to generate all CRUDy functions for each schema, the context module will grow huge. Fortunately, most of the time in a well-designed context, you’re not going to need most of them. You’re probably going to have one, maybe two schemas that need all of the functions, but for all the others, you’re either going to manage them as associations through the “main” schema or you’ll need just one or two of the generated functions.
Furthermore, remember those “collaboration modules” we talked about in section on ecto? You can use something similar - have a set of modules behind the context defining the functions and have context just “reexport” them (for example using
defdelegate). In a way, this is similar to the “Facade” pattern.
I really hope this post clears things up, at least for some of you. The idea of contexts is ultimately a very simple one. It can be summarised as “define modules and functions”. It’s unfortunate so much additional, and in my opinion necessary, additional philosophy was attached to it.
I also recommend watching Chris' keynote from LoneStar Elixir where he first introduces the changes phoenix 1.3 is making and his talk from ElixirConfEU where he talked about contexts a little bit as well (once it comes out).