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.