Are Contexts a Thing?

Underjord is a tiny, wholesome team doing Elixir consulting and contract work. If you like the writing you should really try the code. See our services for more information.

The discussion of Contexts in Phoenix and their general usefulness feels like a common point of disagreement in Elixir. I’ve gathered that the discussions went high and low as it was becoming a thing in mainline Phoenix. I don’t really care for controversy but what I see is a topic which gets confusing to wrangle with and which I never know quite how to explain. So this will be an attempt to explain what Contexts as provided are, cover some common concerns around this and two rather opposing suggestions about how to deal with them. Then I’ll conclude with some kind of nuanced take that doesn’t really help you decide what you believe because that’s your job.

The Default - Contexts in Phoenix

The Phoenix generators are very helpful and if you are generating controllers etc. they will gladly help you generate a Context. If you check the Phoenix documentation you can see what’s generated by default. I’ll add that code below for reference. You can generate your own (this was 1.6.1) with mix phx.gen.context Catalog Product products title:string or get the same as part of a mix phx.gen.html call. The thing that is called a Context is in lib/hello/catalog.ex as the Hello.Catalog module. You can see the file here, I’ll strip out docs, aliases and imports to give a more skimmable thing and then we’ll try to say something about what it is.

elixir lib/hello/catalog.ex
defmodule Hello.Catalog do
  def list_products do
    Repo.all(Product)
  end

  def get_product!(id), do: Repo.get!(Product, id)

  def create_product(attrs \\ %{}) do
    %Product{}
    |> Product.changeset(attrs)
    |> Repo.insert()
  end

  def update_product(%Product{} = product, attrs) do
    product
    |> Product.changeset(attrs)
    |> Repo.update()
  end

  def delete_product(%Product{} = product) do
    Repo.delete(product)
  end

  def change_product(%Product{} = product, attrs \\ %{}) do
    Product.changeset(product, attrs)
  end
end

So a Context Catalog with only a single type of entity called Product to manage can look like this. If we’d add another schema of concern we’d get list, get, create, update, delete and change for that as well. So this is CRUD. I’m not insulting, that’s shorthand for Create, Read, Update & Delete. Plus list which is a common need and something called change which is a bit unusual and relates to making forms from changesets in Phoenix.

The Context is just a module. That is, it is not a required portion of making the Phoenix web framework do web application stuff. Many pieces you deal with in a typical Phoenix app are load bearing functional components. Functional as in “it performs a function” or “has a job to do”, not as in Functional Programming. Your Endpoint, your routes, your controller, your views and your templates are all fulfilling a particular piece of the puzzle. And for database operations your have Ecto providing a number of pieces that do work: Schemas, Changesets, Migrations and the Repo are all there to fulfill a certain functionality requirement. If you generate using mix phx.gen.html the Context will take a load-bearing position in your code, between the Phoenix Controller and Ecto. But it is not itself a required part of making the puzzle work. You can make those same calls from a Phoenix controller and that works fine.

I think this not-absolutely-necessary aspect of the Context is a common place for people to get tripped up. You are in there learning how to make the web app work at all. You need every part, except this piece that gets a bit fuzzy around the edges. What is it? What should I put there? I’m not convinced Contexts are bad. But they introduce design choices for the developer to make into a system that is otherwise very mechanical. Phoenix is full of design choices that are already made for you. Contexts recommend a certain approach but they don’t require any particular approach, there is not forcing function to keep you in line in any manner. While a Phoenix Controller works a certain way, routes work a certain way, LiveView works a certain way. Contexts are not nearly so tangible. You don’t use Phoenix.Context, there’s no there there in a sense.

And not being a mechnically required component doesn’t invalidate anything from being useful code. We would have to strip away a lot of abstraction if we only wanted to deal with the strictly mechanically necessary. For example, Ecto.Changeset is mostly a pure abstraction but it is mechanically pretty much required for operating Ecto effectively. The reason Phoenix is a nice web framework to work with it that it manages a lot of complexity for you, allowing you to opt in to it if you need it while keeping it mostly out of sight for most of your work. So that is a way in which the Phoenix Context sticks out compared to other parts of Phoenix. It is a less clear-cut piece, it is providing a software design for you.

So a Context is not required. Why did it get introduced? What is it for?

The Context is an attempt to provide order that your apps can grow with. It is an attempt to provide a generic way of encapsulating concerns which could make managing your code-base as it grows easier. You can also see it as the bridge between HelloWeb which is your Phoenix code and Hello which is your Elixir code. Essentially Hello.Catalog being your interface into Elixir-code, devoid of web concerns.

I get why there has been a bunch of discussion and contention about whether Phoenix Contexts are good. It is a design and it is a design of “your code” and in that way it is quite different from the design of other parts of Phoenix. It goes a step beyond a minimal generated controller and tries to provide you with some scaffolding for what I imagine is a suggested best practice. It is not a suprise that people have opinions on good software design.

One common criticism of Contexts are that they are a somewhat clumsy abstraction on top of just calling Ecto. Another is that they are a leaky abstraction. So what is this about?

I think we can lean on Saša Jurić to touch on some of that.

Refining Contexts according to Saša Jurić

“The problem with the “official” approach is that the interface concerns leak into the core, blurring the border between the two layers. This defeats the main goal of the design: clear separation between the layers."

This quote is from this worthwile article on how Saša and Very Big Things have worked on making their Elixir code maintainable.

You really should read it to get the full perspective but the key point about leaking here is concerned with this:

elixir
  ..
  def create_product(attrs \\ %{}) do
  ..
  def update_product(%Product{} = product, attrs) do
  ..

These function heads indicate that they take any map of attributes to turn into a product. Usually these come straight from an API, HTML or LiveView controller and contain a map of string keys pointing at varyingly typed data. It is then, inside the Context, turned into proper data via an Ecto Changeset cast, validation happens and can fail. So it is a thin layer on top of the Ecto Changeset API and as an abstraction it doesn’t provide a lot of clarity about what you can expect to do and still have it work. If we send in %{"name" => "Foo"} that’d be correct, %{name: "Foo"} would also work fine, %{"title" => "Foo"} would only fail if the changeset or database requires the name field to exist.

It lets us send in anything and sorts it out. Which is convenient when you need to change things but it doesn’t impose any real separation between Ecto and your Controller or LiveView. And maybe you don’t want those to be separated?

So Saša’s article suggests strictening the interface of your Context by using something like def create_product(name, price, description) do instead where you break apart the attributes and you can actually even guard the values explicitly. So do you abandon Ecto Changesets for casting and validation? How will you render forms conveniently? No-no. The suggestion is to add a separate schemaless changeset in the Controller which knows how to validate and maybe even render the form. And then you wall off using the database parts of Ecto using the Boundary library.

So this takes what is a bit of a sticky layering where Ecto-friendly data structures flow in and out and separates it into something where you get well-structured input. You can have more meaningful typespecs this way as well. The article doesn’t say specifically because it focuses on slightly different things, I imagine the Context’s functions still return some Ecto data structures such as a Changeset on error and an Ecto Schema Struct on success. You could of course strip out the Ecto Schema Struct and reshape that data to be more neutral in this layer as well.

Getting rid of Contexts by listening to Chris Keathley

“I’m pretty sure Contexts are not a thing”

I don’t believe Chris Keathley has put his ideas on Phoenix Contexts in writing. I’ve picked them up from his conversations with Amos and occasionally Anna on the Elixir Outlaws podcast along with when we had him on BEAM Radio to speak of heresy. It was a good episode, he really put us to work discussing design.

Note: Chris has since ducked out of public activity in the Elixir community, if you disagree with my interpretation of his ideas, reach out to me, not him. I’ve found his thinking and ideas immensely useful through the years and that’s part of why I share my interpretation of them.

Brutally paraphrasing I understand his points to boil down to Contexts being a leaky and limiting abstraction. They leak Ecto concerns, both changesets and schema structs, but they don’t provide the power of Ecto’s full API. They also hide implementation detail behind a layer of abstraction where in many cases you’d benefit from having it clearly laid out at the call site.

Chris has mentioned the “call site” a number of times through the episodes of the Outlaws. The call-site is the place in the code where you decide to initiate a particular operation. Often in Phoenix this is a Controller or a LiveView callback. In wider discussions than the Contexts one he has mentioned that the call site is usually the only place that really knows what to do in the event of a failure. Contexts, as generated, don’t really change that, they just return what Ecto would. But if you abstract deeper than those generated contexts you might start running into that. So overall, I take his recommendation to be: give the call-site the capability to explicitly handle errors.

Beyond that he has spoken about using Ecto directly in controllers, building “fat controllers”. The idea being that the Controller action shows you what the entire operation does. Some Ecto operations, some error handling, sending an email to notify users. You’d see the entire flow in the controller. You’d know all the steps it takes by reading that page of code. It won’t require you to keep in your brain the context of additional Contexts. Does MyAccounts.create_user/1 send an invite email? Who knows. Does this?

elixir
# ..
case MyUser.create_changeset(params) do
    {:ok, user} ->
        case Repo.insert(user) do
            {:ok, user} ->
                case Mailer.deliver(MyUser.Emails.welcome(user)) do
                    :ok -> # .. render success
                    {:error, errors} -> # .. render errors
                end
            {:error, changeset} ->
                # .. render errors
        end
    {:error, changeset} ->
        # .. render errors
end

# ..

I’ve intentionally not made this super clean. It uses case statements and stair-steps heavily as a result. But is it unclear what it is doing? I don’t think so. Is it elegant? Not really. It is tempting to pack these things into abstractions and build neat functions. But I know there is benefit to being able to just read the flow directly. Many small functions can lead to a lot of jumping around and at a certain point threatens sanity in the same way that OOP inheritance does.

So this is very explicit and it is leaning on the necessary abstractions such as Ecto and a Mailer that both manage other systems. It doesn’t introduce additional abstractions that concern what we are trying to do, creating a user. I think this is important to be mindful of when creating a lot of nice abstraction layers. What is being made easier? What is being made harder?

Some kind of conclusion

I don’t actually care which one you use. I sometimes barely care which one I use. But when building a code-base which is intended to grow and be shared it is important to be mindful of what approach you are taking. The fact that multiple people with rather opposing positions see problems with the Contexts design as it is generated tells me it is definitely worth considering whether it really serves your purposes. It could also just be that it is a bit middle-of-the-road and as such not satisfying either end bell curve. When it comes to opinionated approaches I think it is helpful to know which opinion you lean towards. Mixing them is probably the worst approach. I think being consistent beats using the exact right thing in many cases. I’m partial to (my interpretation of) Keathley’s approach because it involves less code, fewer layers and actually defers the option of adding abstraction layers to a later point.

I think there is good stuff in Saša’s approach. It seems like it improves on the separation, makes the layering more meaningful and in that way fulfills more of the purpose of the original design. I especially like the honesty in suggesting you probably want a tool like Boundary to enforce the layering. It is very easy to shortcut through a layer like that and break through that design and having an automated tool tell you when you are breaking your own design is useful.

What other consequences are there? With the explicit in-controller approach you would likely only write one set of tests, for the controller. With the Context approach you’d likely be writing tests for both the controller and the Context. And with a thin controller they’d be very similar but likely not the same. Oppositely, if you provide multiple interfaces into your system, several types of API, webhooks, Web UI and they all need the exact same functionality, Contexts offer the potential for re-use which isn’t part of the explicit in-controller approach.

So what do I recommend? Try one and see how you like it. It won’t matter for a throwaway project, as the challenges here are mostly about growing and maintaining a code-base, but you might get a sense of how it is to work in that way. To me this is mostly lines up to be a division of values. Do you prefer more structure and more abstraction or less? This is a rather nuanced piece of software development and involves all the good topics around hiding complexity versus making the code clearly readable in a single function. Essentially eternal topics.

If you don’t know what to believe. You’re just trying to get your web app to work. I’d just as well roll with Contexts as generated. This type of nuance in design can be meaningful for teams and growing systems, I wouldn’t put it high on the list for a new learner. As far as making the web go brrrrr, it doesn’t matter.

Underjord is a 4 people team doing Elixir consulting and contract work. If you like the writing you should really try the code. See our services for more information.

Note: Or try the videos on the YouTube channel.