How would you explain Phoenix Contexts to a newbie?

If a newbie asked you about Phoenix Contexts, how would you explain the basics to them?

Feel free to be as concise or in-depth as you like…


In short:

Phoenix Contexts are just dedicated modules that expose and group functionality.

Official Docs:

https://hexdocs.pm/phoenix/contexts.html

Also see:

5 Likes

Contexts are just the explicit application of behavior to data.

If I am the owner of a factory then I’m going to assign a worker to a task.

Under another train of thought, a worker would get assigned a task.

Either way that worker isn’t going to go to work on the first day and assign himself tasks and know what to do. As the owner of the factory I know why I hired him and what task he is supposed to perform. There’s no practical difference between the two.

The differences are subtle, but in both cases the context is the factory. I’m the first case I’m making that context explicit, “I, the factory owner, am assigning the worker this task.” The subject in this sentence is the factory owner, the verb is what the factory owner is doing.

Whereas in the second case I have the implicit association, “The worker gets assigned this task.” I assume it’s at the factory, I but the subject of the sentence is the worker and the verb is the assignment.

It’s the same in practice, but it’s how explicit I am about it.

It’s not a perfect metaphor, but that’s the point of metaphors. Who can ask for perfection when comparing two unlike things?

Note: Sorry for those with English as a second language. I just find this easiest to explain using sentence structure as a teaching tool.

3 Likes

I would use a simple basic crud operation (create a customer) and the corresponding code (Repo stuff like phoenix 1.2) in a controller action. I’d ask the person what would need to be done for the new CLI tool to be able to also create a customer given the same input. The answer obviously is not using a fake http call nor duplication of all the Repo calls. Therefore a new layer (a customer.ex maybe?) is needed above the phoenix controllers and the cli, so both can use it. I’d explain that this layer is for all the business logic of the application and the CLI and phoenix (http) are simply two different interfaces between the user and this business logic. So everything which stays the same for both interfaces likely belongs in the application core, whereas everything that’s different belongs in the phoenix controller or the cli controller.

Now the most important part is explained. The business logic shouldn’t be in the controller. But as soon as the project gets a bit bigger / developed further, the business logic of the application’s core might growing beyond a handful of business logic modules. That’s when all this logic can be split up into contexts. For contexts it’s important to understand that they are subject to the domain of the application and should be chosen with a bit of care and knowledge about said domain.

The added benefit of those contexts is no longer to avoid duplication, but to avoid tight coupling. E.g. a customer might have it’s own webinterface, which needs to know totally different information about the customer, than the help desk desk clerk needs if the customer does call. Therefore there can be a webapplication/customer and a crm/customer. There might be some duplication between both, but each can be changed/developed independently from the other one. Additionally it’s easier to change something / reason about something, which touches only a part of an application, instead of the whole application.

It’s also important to note, that this is not an all or nothing situation. If the application domain is small contexts do not really have much benefits and a functional language makes it relatively easy to split things into different contexts later when needed. But if it’s needed contexts are a good solution to work with and knowing about the idea should make it easier to identify their need before the application’s code is a giant mess :smiley:

5 Likes

I would say that context are where you put the functions that transform your internal data into something the web layer use. Like what you need, how to get it, how to order it, etc.

So that the web layer as a nice and known way to use it. Like if you had to define the API you use with your front end dev.

2 Likes

Contexts are a conceit to establish boundaries between areas of your application that have to collaborate but benefit from only being minimally coupled.

There is nothing Phoenix specific about context. The greatest impact of a context is that it establishes a (service) boundary that identifies the capabilities that are being offered to the “rest of the world” while it entirely conceals the means by which those capabilities are realized. So Michał Muskała is correct when he states quite simply that contexts are:

modules and functions defining public interface of your application.

The name bounded context (highlighting that the boundary is the important aspect) of course dates back to Eric Evans’s Domain-Driven Design: Tackling Complexity in the Heart of Software (2003) but really has more recently become more relevant because bounded contexts are a core concept in the microservices design philosopy even though the concept itself trancends microservices and really is relevant to any larger scale application design. So Phoenix adopting contexts could possibly have some correlation to it’s maturation during the rise of microservices.

The problem for neophytes is that the idea behind context is somewhat abstract and that it is the result of a body of experience that they probably haven’t had the opportunity to be fully exposed to yet. And it also doesn’t help the identifying the optimal boundaries up front can be challenging even for experienced developers and architects - especially if it’s an activity they haven’t consciously pursued before.

This is why Building Microservices uses a case study for demonstrating contexts.

The case study involves the fictious MusicCorp which has business activities the cover managing orders (with S&H), managing stock, manage payroll, manage accounts - all of which leads to lots and lots of reports.

Right off the bat there are two clear contexts: finance (manage payroll, manage accounts) and warehouse (manage orders, manage stock). Either of them conduct an awful lot of detailed business activities that the other doesn’t need to know about - but to accomplish the day-to-day business they have to rely on one another’s capabilities (e.g. finance needs to know about the orders and stock levels in order to manage their accounts but actually managing the orders and stock levels is the warehouse’s job) which implies that there is information that is exchanged via a shared data.

Now the data shape of a stock item that the warehouse shares with finance will be different than the shape they use internally because

  • finance doesn’t need to know all the gory details about “stock item”
  • warehouse needs the freedom to change the internal representation of “stock item” (data) in order to adapt to ever-changing business needs without constantly impacting finance

This really is the same kind of thinking that also motivates the interface segregation principle and consumer driven contracts; related capabilities (no less and no more; high cohesion) need to be grouped together so that any shared information leads to low coupling between consumers and the producers.

So in general a well designed context doesn’t let internal implementation details leak out in order to maintain autonomy over how its capabilities are implemented and to isolate its consumers as much as possible from any effects of internal changes.

One of the takeaways is that contexts will often have separate representations for same shared concept (e.g. “stock item”) - one representation to share with the “outside world” and a much more detailed internal representation to support their own capabilities operations.

However the shared data representations are actually the lesser aspect of a context. A single context groups related capabilties. Sam Newman actually warns that focusing primarily on the data too much tends to lead to primarily CRUD-based capabilities.

So ask first “What does this context do?”, and then “So what data does it need to do that?”

Also contexts can exist on different levels of granularity - finance and warehouse are considered course-granularity contexts. These in turn can break down into distinct fine-granularity contexts. For example the warehouse context could be divided up into:

  • order fullfillment
  • inventory management
  • goods receiving

And whether or not the finance context (or one of it’s own (sub-)contexts) deals with the warehouse context or the inventory context directly is a design decision that can go one way or the other depending on the circumstances.

And because contexts collaborate they are also about compositionality. In terms of Elixir/Phoenix I expect the “face” (aka public interface) to be an Elixir module - but it is important to remember that the implementation of the context’s capabilities could require a handful of modules, an army of modules or possibly even multiple OTP applications.

Contexts aren’t a one-size-fits-all cookie cutter solution. And the value of high cohesion (and consequently loose coupling) was never a monopoly of the OO paradigm - it applies to the functional paradigm as well and even more so for distributed applications.

So contexts that are too small or for that matter any other form of choosing the “incorrect” boundaries will invariably lead to tight coupling because the separated capabilities need to share too much detailed information (however there are lots of other causes of tight coupling). But that doesn’t mean there won’t be small contexts. So it would be incorrect to assume that a “small” application can’t have multiple contexts.

In the case of Building Microservices the recommendation is to actually start with a monolith (egad) if there is no clear way to identify the necessary context boundaries but to be committed to splitting off the microservice(s) (i.e. context(s)) as soon as the boundaries start to emerge. This is actually in line with reports from Domain-Driven Design: Tackling Complexity in the Heart of Software where for DDD projects some key (domain) insights only revealed themselves some time into implementation and there is only one way to deal with it: Refactor Early, Refactor Often, Refactor Mercilessly.

The rub of course is that one cannot expect a new (unsupervised) developer (including expert-beginners) to catch these boundaries and key insights “revealing themselves” early - so they are more likely to keep descending into the “Big Ball of Mud” - investing way too much time into the wrong path. As far as I can tell “defining context boundaries” isn’t something that can be captured in a “Pattern-style Template”. While it is usually easy to recognize a well designed context, creating a well designed context typically is not that easy.

Typically it starts with learning how to recognize and deal with inappropriate intimacy and then develop that to recogize appropriate and inappropriate tight coupling. It’s natural for capabilities within the same context to be tightly coupled because they are closely related (cohesion, which is why they should be in the same context in the first place). But when you encounter tight coupling between contexts there is a problem - it could indicate that a capability has been separated from its context or it may simply mean that the context’s interface is unnecessarily revealing of its implementation details.

29 Likes

In a database design, you can end up with hundreds of associations to individual records or data types over time. A standard ORM layer wants you to remap all of these relationships so that your application can wrap them in objects. It’s a completely redundant process that gains you little over raw queries and continuously builds “what was this field for again?” questions for every new developer.

Contexts take the independently related parts of your data and group it into real subsets that actually match the application instead of just being a duplication of your database schema. Once a context is built, you may never touch it again while other parts of the application continue to grow and change using the exact same tables. If any new developer jumps in they will see the specifically related parts and know that they all matter to this part of the application.

As a bonus, you won’t bother querying columns you don’t need from huge tables, saving on memory and latency.

4 Likes

I find it easier to understand and attempt to demonstrate a new concept by using a worked example. Let’s take the ubiquitous ecommerce website to go through.

Before the introduction of contexts, we’d likely create Phoenix models (Ecto schemas) for our users, products, orders, line items, etc. All bundled together inside web/models. Each model would include concerns related to the entire shopping domain. As an example, the user model would include a billing address, shipping address, and login details.

With contexts in Phoenix 1.3 we would spend additional effort up front deciding how to separate our entire shopping domain into its constituents. This decision is usually driven by the business requirements, often mirroring the physical departments in the business.

So we might decide to create our contexts as follows:

  • Accounts to manage user registration and authentication/login.
  • Product Catalog to allow listing of new products, displaying products for sale.
  • Cart to deal with visitors adding products to their shopping cart.
  • Orders to take the visitor through the checkout process and confirm their shopping cart.
  • Billing to actually charge the visitor for the items in their cart.
  • Shipping to handle the physical, or electronic, delivery of the paid for goods.

A context may share a model (Ecto schema), such as a user, but they will access different fields. For example, the accounts context uses the user’s login/email and password; billing uses their name and billing address; shipping uses their postal address. This demonstrates the shared database table, but different model in each context containing a subset of the entire fields (columns).

By investing the time in defining our contexts we get a number of benefits:

  1. A folder structure that replicates the application we’re building, namespaced per context (e.g. billing, cart, accounts). Any of these could later be extracted into their own Elixir application, contained within an umbrella project, should they become too large.
  2. A context provides a public API. This allows the internal implementation to be somewhat hidden and allows refactoring. Consumers use the public functions, via the context, not the internals.
  3. Contexts help developers locate the code they need to change when adding features or fixing bugs. As an example, if there is a problem with a billing feature, I can immediately navigate straight to that context. I’m also guided as to where new modules and functions should be written. This should help beginners, and experienced developers alike. Since a context is a smaller part of the entire business, there is less mental overhead when working within it.

For more background on the subject, I recommend watching Uncle Bob’s Architecture the Lost Years talk where he explains this concept in detail. It is also the origin of the phrase “Phoenix is not your application”, albeit applied to Rails in his case. He simply means that when you look at the top level folder structure it should indicate the problem domain, not the framework. So we should see a directory structure I’ve outlined above: accounts, product catalog, cart, billing, shipping. Not models, controllers, views, templates. The web folder is a separate context, and one that just provides an external HTTP interface to the other contexts.

In summary, I’m entirely in favour of contexts in Phoenix 1.3. Thanks to @chrismccord and team for taking the time to introduce them, and helping to guide Phoenix developers into a more maintainable approach to building web applications.

26 Likes

It’s also far easier to be explicit about your application’s boundaries, using contexts, up front than attempting to apply them afterwards. You will inevitably discover a tangled mess of interdependencies if these separations are not made explicit and concrete from the outset. That would be my argument in favour of including contexts in the new Phoenix 1.3 generators. It’s worth the investment of your time to think about these boundaries.

Some brilliant explanations here - thank you! Definitely see some budding book authors here :wink: :003:

Please keep them coming! :023:

(Really keen to see a short overview/paragraph about Contexts if anyone is up for it…)

Here’s a concise explanation I’ve had some success with before:

The console (iex) can/should be thought of as a 2nd UI layer of parallel importance to the web UI. If complex business logic is only in controllers, then you have to duplicate that in the console. If complex business logic is in context modules, then you get to call them and reuse that logic from your console UI layer.

Contexts act as that reusable layer for business logic. The names of modules/functions/parameters should reflect business domain concepts.

21 Likes

Hah, I love this console/iex analogy, it is perfect! Proper Separation of Concerns. ^.^

6 Likes

Thanks for clarifying this topic. It helps me better understand context now!

1 Like

Contexts are to Controllers how Presenters are to Views.

Presenters encompass all the stuff that the view will consume.

Contexts encompass all the stuff that the controller will consume.

2 Likes

Is Context special to Phoenix? I though It just Elixir module or namespace. Am I wrong?

1 Like

In the book-programming-phoenix they say (about Contexts):

All good application programmers must learn how to break down complex ideas into discrete steps. The opposite is also true. To build a beautiful API, you must be able to coalesce discrete functions into ideas by strategically layering and grouping functions.

Every library we use, and even Elixir itself, is structured based on those ideas. For example, any time you call Logger.debug from Elixir’s standard library, you are accessing the Logger context. Internally, Logger may be broken into multiple modules, but for us, everything is exposed through a simple, well-defined public API of the Logger module.

Phoenix projects are structured like Elixir libraries and projects.

If you haven’t got the book I highly recommend it! I’m hoping we can start a book club for it soon, if anyone’s interested in joining, please let me know :023:

1 Like