This is not a regular finished-and-refined article, just a quicknote. A brain-dump, if you will. Read at your own risk.

Contesting Contexts

by Paweł Świątkowski
18 Jun 2023

Today I have read an article “Kill Your Phoenix Context” by Peter Ullrich and here are some thought about the ideas in it.

The article resonates really well with what I think about Phoenix contexts. In general, I really like that Phoenix team decided to include them as a default code organization utility of the framework. It’s a great step forward from the traditional way of trying to fit everything in framework’s machinery, such as models or controllers.

However, on the other hand, what Phoenix proposes by default is really simple. It works for simple CRUD apps, but I have similar experience as Peter: when the app and the complexity grows, most context become a dumpster for resource-related code, with no internal organization.

I emphasized “resource-related” because it’s the second part of what I don’t like. Context as Phoenix promotes them are resource oriented and I believe it not a great way to structure the project in a scalable way.

Example time!

Let’s say we’re working on a travel agency portal. It offers some trips, with different accommodation options as well as some optional activities. You can reserve a trip for a given number of people, select options you like… I think you can imagine it, more or less.

In such an app you will surely begin with a Booking context. After all this is the central part of your business domain. It easy to imagine having functions like:

  • book_trip(trip_id, people, options)
  • cancel_booking(booking_id)
  • add_optional_activity(booking_id, activity_id, options)
  • change_accommodation(booking_id, accommodation_option_id)

The list might be slightly longer, but this is good for a start.

These are great functions! They touch a core of what your company does - allows booking trips and managing the booking. But this is not the only stuff your context will have. In fact, this will be a minority of it.

Most of a vertical space in the file will be occupied by other “context-default” function, such as:

  • get_booking(booking_id)
  • get_user_bookings(user_id)
  • get_cancelled_bookings_from_last_days(num_of_days)
  • get_bookings_for_trip(trip_id)

You can notice the difference. These functions don’t do anything, they only exist to fetch some data and return it, most probably to a controller that requested it and will now render some HTML or JSON.

Back to the post

What Peter suggest is to make this separation “official” by moving the code around. The get_ functions would go to App.MyContext.Finders namespace, while the functions from the first section to App.MyContext.Services. Aside from that, he proposes the third namespace: App.MyContext.Repository for CRUD operations. I think this proposal is quite good, although I would make a few comments or changes:

  • Rename “services” to “commands” - services has a bad publicity in software architecture and I think there’s a good reason for that. The name is quite generic and if we can find a better name, let’s maybe use it.
  • Get rid of “repository” - if your context has simple CRUD operations (other than fetching by ID), it probably is a weak supporting domain. At least that’s my experience. Things like that are usually necessary in every software project (comments, support requests, abuse reports…), but they are not really worth spending too much time on them. If something is CRUD, it’s simple.
  • I’m used to call “finders” “querying”, but this is just my personal preference. Probably fueled by the fact that for example in Rails, in which I have a lot of background, find method family return only one result.

But then there’s a whole issue where I think this proposal is not radical enough ;)

Enter CQRS

Wait. CQRS? Isn’t it the thing related to event sourcing? I’m not doing that in my project, so why would you bring it up?

It is partly true. CQRS is usually collocated with event sourcing, but the truth is that you can as well do event sourcing without CQRS as you can do CQRS without event sourcing. In principle, CQRS - which stands for Command-Query Responsibility Segregation - makes a really valuable observation about almost every software system on the planet: that is consists of queries and commands. And that they are really different beasts.

  • Commands are changing the state of the system. They are doing things. It’s the heart of the application. It’s the first part of our list of functions above (book_trip etc.). And, as I like to point out, a list of commands is basically a list of the application’s capabilities.
  • Queries, on the other hand, are not so crucial. Sure, our travel app need a way to list “my bookings” and the backoffice operation needs some kind of a table listing all new/confirmed/cancelled/unpaid bookings. But they are, in my opinion, secondary to commands for two reasons:
    • If there are no commands (= state changes), there’s nothing to query. We just have a static website.
    • They are rules by UI design. If you have a view that lists bookings made in last 30 days, you need a query to support that. By then if a designers decide that it’s not needed, you don’t. It changes from quarter to quarter independent from what the business actually does.

So, to represent this hierarchy, I would propose the following rules for designing contexts:

  1. The root context module contains only true business logic, which is commands
  2. Queries are moved to a separate submodule: App.MyContext.Queries. And the function in there returns just Ecto queries, not a list of structs.
  3. Whoever needs to query for data, uses App.MyContext.Queries directly and is responsible to call Repo.all on them (or some other function making paginated results etc.). These places would usually be controllers, GraphQL resolvers etc.

Point 3 may sound like going against everything we believed in - that we should only call context’s root module functions and everything under it is considered private. But the truth is that thing like pagination do not belong to a business logic. You are only paginating because it’s common to have HTML views paginated. It’s a view requirement, not a business logic requirement, and as such - it should remain close to the presentation layer.

This way queries are made secondary (sorry, queries). But when we open our App.Bookings context, just by listing its functions we get all app’s capabilities related to bookings. No noise from 167 ways the data needs to be queried because of many design requirements. Queries can still be composable (this is what Ecto is about). And sure, we may not like that a controller needs to call Repo directly, but I think it’s a decent trade-off.

Proof by Absinthe.Relay

The idea to separate Queries submodule and allow it to be a public interface came from a time when our team was integrating Absinthe.Relay for the first time. This is a library for paginating in GraphQL and its most important (at least for us) interface was Absinthe.Relay.Connection.from_query/4. As you might imagine, it takes an Ecto query and applies some additional limits and offsets to make pagination work. I’m pasting an example from the docs here:

alias Absinthe.Relay

def list(args, %{context: %{current_user: user}}) do
  Post
  |> where(author_id: ^user.id)
  |> Relay.Connection.from_query(&Repo.all/1, args)
end

Now, imagine you want to put it in the context. Are you going to call Absinthe.Relay in the context? No way. This is just an interface to your data and a very specific one. You don’t want it to leak it into a business domain. Having a root context function returning just an Ecto query does not sound good too (at least for me). But maybe, in the spirit of Ecto’s composability, you could have this in the resolver?

def recent_bookings_by_user(args, %{context, %{current_user: user}}) do
  App.Bookings.Queries.by_user(user)
  |> App.Booking.Queries.not_cancelled()
  |> App.Booking.Queries.from_most_recent()
  |> Relay.Connection.from(query, &Repo.all/1, args)
end

To me it looks like something pretty good.

Anyway, this is my way too long comment to Peter’s post. Whichever way you choose, it will probably be better than just using a context as a place for every function related to this resource. I haven’t really elaborated why I consider resource orientation as a bad practice for designing context, but that’s exciting - it means there will be another note on that!