From Models to Contexts in Phoenix 1.3.0
Phoenix 1.3.0 has brought significant changes to the directory structure of applications and with it the promise of a more natural order to your files based on the design and architecture of your particular domain. However, the migration to Contexts from Models is not mechanical — you must do some work to evaluate how to represent your existing domain logic and schema declarations in Contexts.
If you’re not caught up with the development in Phoenix 1.3 have a look-see at this great overview:
In addition to sundry other changes to the directory structure of a Phoenix application, a significant change is the elimination of models as a concept. Instead, the expectation is that you’ll organize your business logic into contexts that make sense for your specific application.
This article will dive into how we migrated our nascent analytics platform from models to contexts when we upgraded to Phoenix 1.3.0. We’ll discuss the rationale we used to move from our specific models to other specific contexts, as well as choices we had to make along the way and hopefully provide some guidance for how your team can think about your own business logic.
We’re lucky that our code base is only a month old and therefore we don’t have an overwhelming number of models that needed to be migrated. The application’s young age, however, also grants us the opportunity to talk about it in its entirety.
It should be noted that before we started this work to convert models to contexts, we followed the Phoenix 1.2.x to 1.3 upgrade guide. The end result of that work was that our code was all moved to the new directory structure except the models, which still sat under the web directory.
Our application is a simple analytics platform to log events from the various platforms of our application: Cardigan. Yes we could (and do!) use Google Analytics, but we were looking to provide a training ground for a variety of technologies we’d like to start using regularly. We wanted to give various team members exposure to Elixir, Phoenix, React, Redux and GraphQL on an internal but nontrivial project. It’s still under development and close to MVP, but we wanted to upgrade to Phoenix 1.3 and move to contexts sooner rather than later so we could get used to the new paradigm.
The models we had thus far created in the application implemented user authentication as well as organization of entities that could have events logged against them. So users have a many-to-many relationship with accounts, and then an account has many products, a product has many properties and a property has many events.
I’d read a very interesting blog post on how to organize functionality that crossed multiple models and had started to use a services abstraction to encapsulate such behavior. For example, when a User
is created we want to create a default Account
as well. This requires us to create three database records: the user, the account and the join table record. Instead of cramming all of that code into the controller we instead create a function in the RegistrationService
module that does the work. I was relatively satisfied with the division of logic, but found that contexts provided an even more satisfying design.
The first task when figuring out how to break our models in contexts was to decide where the line was between user authentication and our core analytics business objects. The distinction lay somewhere around the Account
model but we weren’t sure if Account
itself was more in a authentication context or more in an analytics context. Ultimately, we decided that the fact that accounts served to organize user access to functionality meant that it belonged in an auth
context.
From there it was relatively simple to break down the rest of the models into either the auth
context or the analytics
context.
To the auth
context went things like Session
, User
and Account
. These were all primitive concepts used to authenticate and authorize a user against business objects.
To the analytics
context went Product
, Property
and (soon) Event
. These are the core domain objects that the auth
context is guarding access to.
Finally, the operations
context was created to scope access to the Health
module which we use to generate the payload of health checks.
The resulting structure was more like what you see here; now we can discuss what happened to our services.
One thing I didn’t quite understand when I started reading about contexts was what the context file itself represented. For example, if you have an analytics
context then there should be a file named analytics.ex
in the directory which defines the context interface. Then I realized that this was the services concept that we were already using. The context file/module defines the high-level API that you will invoke from controllers, etc.
In our case the ProductService
and PropertyService
functions neatly slid into the Analytics
context, while the RegistrationService
function went into the Auth
context. It just made sense that instead of calling
RegistrationService.register_user(user_params)
we’d instead call
Auth.register_user(user_params)
We should note that the validation.ex
modules in the directory structure you see pictured are definitely misnamed. As we were attempting to move authorization-ish functions out of the models in order to keep them as schemas-only there was confusion as to where to place them. It’s likely we’ll end up moving them to some sort of authorization context in the near future but we’ll see how they fare where they are for now.
Alongside the move from models to contexts we wanted to ensure that the test directory structure continued to reflect the organization of the application code.
We ended up replicating the structure underneath /lib
to /test
in order to keep the tests in the namespace of the module under test.
We again replicated the context structure underneath /test/support/factories
so the ExMachina factories we’re using line up with the contexts the schemas are defined in.
Finally, to scrub the models concept utterly from our repo we needed to rid ourselves of /test/support/model_case.ex
. We chose to rename the file/module to /test/support/context_case.ex
and ripple the change out to all of the context tests. It was a bit tiresome, but we no longer have any references to models anywhere in our code.
And thus we’re now fully upgraded to Phoenix 1.3.0. Here at Adorable we’re really excited about this change as it’s a step in the right direction from the Rails problem where your /app/models
directory contains a flat list of 50 files across many domains and sub-domains of responsibilities. The elegance comes with a price though: your team needs to sit down and discuss how your models break down into schemas and contexts. It’s a grey area in terms of architecture because sometimes there is no one right answer to where a schema belongs. In those cases we suggest moving in the most reasonable direction and then seeing how the code evolves. Moving a schema from one context to another is not the end of the world in terms of code change, and sometimes it’s simply not possible to predict how a schema that straddles contexts (think Account
in our example above) will evolve with the code base.
Zachery Moneypenny is a Principal Developer at adorable.io. Rubyist for a long time, with experience in Golang and Javascript as well but moving towards Elixir and loving it.