Applications are built to solve problems - real-life problems encountered by business. More often than not, a single application is delivered to solve multiple problems. For example, do you know how many issues need to be solved to run an online bookstore?
- Product catalog,
and that's only the happy path. We can easily add:
- Promo codes,
or any other feature.
Having them all implemented in one application may lead to questions like:
- Why is wishlist logic defined in the same place as the one for invoicing? What do they have in common?
- Why does a shipment model need to know how to update warehouse stock level when a shipment is cancelled?
- Why does a product catalog model know to which shipping category it is assigned?
- and so on...
By the way - these are examples from a very well know ecommerce written in Ruby on Rails. It's not readable or extendable. The tangled mixture of features and responsibilities is the reason why JIRA tickets take longer to complete as an app grows.
It's time to organize the features neatly so that each problem is solved in its own realm. Let's stop developing big balls of mud. Let’s learn about bounded contexts.
Problem space vs solution space
Before we get to what a bounded context is, let's describe what it solves.
In Domain-Driven Design, the term "domain" is omnipresent. It describes at least 3 different beings (Business domain, Core domain and a business logic layer). Let's concentrate on the first of these meanings; Business Domain is what an organization does and the realm it does it in. It may be "car sharing", "dog grooming", "karate dojo" or "party planning". Whatever is the core interest of your business, it is your business domain.
In order to successfully run a business, you need to take care of all its elements. When running a dog grooming business, you probably need a client (dog?) catalog, a list of employees, inventory of resources and a booking system (to name a few). Each of these areas is a problem to solve. In DDD, they are called Subdomains. They have nothing to do with programming but everything to do with real challenges.
Bounded Context is the solution space for each of the subdomains. It's the application (or its part) which solves a problem. It is the code which is implemented and the language used to implement it. In fact it is crucial that the language is precise and consistent inside of context's boundaries. The boundaries of language consistency define the borders of your bounded contexts. But let's leave it for a bit later...
Problem space discovery
Imagine you want to model an application for planning parties for various clients.
You have been working in this business for a while so you know that before you do any work, you need to have a signed contract with all the terms and agreements. Then, you can plan and organize parties for the client. Knowing that, you know that the final solution has to aid both - contract negotiation and party organization. Therefore, you can create an application solving both problems:
In most Ruby on Rails applications, you would immediately
- create a Client (or User...) object and
- attach it to a contract with
- associate parties with
- and feel good about yourself.
However, when looking closely at such a design, there are questions rising:
- Should the entity which negotiates a contract be called a Client? It's not really a client when it doesn't have a contract signed.
- When contract is being negotiated, should the potential client have parties attached? Not every contract is eventually signed.
- Are contract negotiations details ever needed for planning a party, or only the final form of the contract?
- And many more...
Knowing all that, let's organize the problems into separate spaces:
Bounded Context as a solution space
Thanks to the deepened analysis, performed in the previous paragraph, we can now design a solution for each of the defined problems.
We can create a Party Planning context. It would need some form of a
Party object, to hold all the arrangements. It would also need an Organizer and perhaps Attendees.
In such a case, there might even be no client in this context. The Client is a term which defines your business relation. When it comes to a party, this term may not be used at all! Also, there would never be any mention of a contract negotiation. There might be a Contract object, holding guidelines for the cooperation. But its internals and negotiation history are irrelevant.
We can also create a separate Contract Negotiation context. In there, we can create a Client entity. However, such a client knows nothing about parties. It knows only about the contract. Even better - there might be no client at all in here. Maybe a contract model would be sufficient and a (potential) client is just a value object? Also, a contract in this context is a sum of all adjustments and amendments introduced by the sides. It's the heart of this context. Only when it's settled, it may trigger creation of a client in some other context.
Please note, that in this case, a bounded context has been mapped per each subdomain. This is the ideal situation. In most cases, it would be the best to aim for it. However, it's not always possible or practical to achieve it. It may be too costly in terms of money or resources. There may be too small return from such an investment and you may decide to host two subdomains in one context. Still, it should be a conscious decision.
It's not always easy to separate bounded contexts properly. There are skills, techniques and heuristics which can help you with that. Their details go far beyond the scope of this article, but be sure to look for them when you are ready to follow this path.
"The Ubiquitous Language is a shared team language. It's shared by domain experts and developers alike. In fact, it's shared by everyone on the project team."
— Vaughn Vernon, Implementing Domain-Driven Design
Every bounded context has its own domain model. The names, definitions, behaviors and descriptions are bounded to a specific context. Therefore, each context has its own ubiquitous language. It's omnipresent, but limited to a single boundary.
Context-bound language allows to have different representations of the same entity in different areas of the solution space. In the discussed party planning example, the term "contract" has a different meaning in each context. It is expressed using two different models. Both "contracts" may represent the same being, with the same contract number. However, the characteristics and behavior of each would be completely different.
Each context may be implemented and maintained by a different team. Each of the teams would have their set of expressions and domain-specific terms. All of them, would be expressed in bounded contexts’ implementations and used on a daily basis in conversations as well as in code.
What is inside of a Bounded Context?
"How many Modules, Aggregates, Events and Services (...) should a BC contain? (...) A Bounded Context should be as big as it needs to be in order to fully express its complete Ubiquitous Language. "
— Vaughn Vernon, Implementing Domain-Driven Design
The primary occupant of this conceptual container is a domain model. The definition of how the business world is modeled inside of the piece of software. It describes the rules and the beings needed for the specific realm. However, a bounded context is not limited to the model.
When there is a need to persist the domain modal, the persistence details (ie. database schema) lives inside of the context as well. In plain words - if you store any data, you have to also define how the storage is performed.
When there are User Interface elements, which render the model, these are also placed inside the bounded context. Those may be views as well as their elements which trigger new actions within the boundary.
If the context exposes services (ie through API, SOAP or some messaging endpoints) the service-oriented components are inside the boundary.
Finally, when there are client-facing resources defined (ie. API), there needs to be a bridge between that implementation and the domain logic. In DDD, this role is fulfilled by Application Services, which are also a part of a bounded context.
Bounded Context in Ruby on Rails
As great the theory is, it often doesn't match the reality of a Ruby on Rails application. Still, there are at least several ways to express a bounded context. Each has its pros and cons.
The most straightforward way is to simply create separate applications. It brings the clarity of boundary distinction. It lets to split contexts between teams and allows for uninterrupted development for each of them. Such a solution brings minimal coupling between contexts. However, some coupling may be beneficial. It may reduce code redundancy, increase productivity and allow to share solutions which exist outside of a bounded context - at the level of an application.
There are many Ruby on Rails applications, where one team has to take care of several contexts. In fact, this is one of the major reasons why such teams tend to create vast contexts, containing the entire problem space. For such an occasion, it is possible to distinct bounded contexts using modules. You can use a folder structure to organize the contexts. There are many different possibilities, but my personal favorite is something similar to:
- app/ - controllers/ # for elements which are not modularized - models/ # for elements which are not modularized - modules/ # name of this folder is up to you, it may also be dog_grooming - contract_negotiation/ # bounded context's name - models, services & persistence definition # rails'y folder structure - party_planning/ - application, domain, infrastructure # layered architecture - ...
Modules with Rails engines
The modular approach may be further enhanced by using Rails engines. However, RoR developers tend to have a polarized approach to those - they either love or hate them. Rails engines provide a good level of separation between contexts. Each engine may be tested in separation, which makes sure that they don't blend together. I see that as a great benefit, which may be put to a good use. However, I personally don't have good experiences with engines. I mentally associate them with something deeply coupled with Rails framework. My ideal bounded context definition is written in (almost) pure Ruby. Using engines doesn't contradict it, but it may be one level of abstraction too much if you are not familiar with them.
By all means, check them out - but leave it for later if they are not your go-to tool in the first place.
Modules with Packwerk
If you decide to use modules, please take a look into Packwerk from Shopify. Lately, it has gain much popularity. It allows you to maintain coupling between modules in an organized manner. Quoting their readme file: Packwerk is a Ruby gem used to enforce boundaries and modularize Rails applications.
Similarly to engines - it may be an extremely useful tool. But use it once you have a settled approach to modularization. It's an aid, not a solution.
It is a good idea to modularize your applications, even if you don't follow domain-driven design in your daily practice. Bounded contexts are one of the most important concepts in Domain-Driven Design. They allow to explicitly define boundaries for a model, when multiple models are in play. They help to keep the models consistent and precisely tailored for the needs.
- Eric Evans, Domain-Driven Design: Tackling Complexity in the Heart of Software
- Eric Evans, Bounded Context - DDD Europe 2020
- Vaughn Vernon, Implementing Domain-Driven Design
- Martin Fowler, Bounded Context
- Example Ruby on Rails application with bounded contexts
- Packwerk from Shopify