29
.
04
.
2024
25
.
01
.
2023
Ruby on Rails
Domain-Driven Design
Backend
Tutorial

Bounded Context - DDD in Ruby on Rails

Paweł Strzałkowski
Chief Technology Officer

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,
  • Checkout,
  • Shipping,
  • Invoicing

and that's only the happy path. We can easily add:

  • Wishlist,
  • Promo codes,
  • Returns,
  • Newsletter

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:

Too large bounded context

In most Ruby on Rails applications, you would immediately

  • create a Client (or User...) object and
  • attach it to a contract with has_one :contract
  • associate parties with has_many :parties
  • 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:

Distilled subdomains

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.

Bounded contexts with subdomains

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.

Ubiquitous language

"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.

What is inside 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.

Separate applications

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.

Modules

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.

Summary

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.

Resources

Articles in this series

Do you want to know more? Register for our DDD webinar

Paweł Strzałkowski
Chief Technology Officer

Check my Twitter

Check my Linkedin

Did you like it? 

Sign up To VIsuality newsletter

READ ALSO

LLM Embeddings in Ruby - Paweł Strzałkowski

LLM Embeddings in Ruby

17
.
03
.
2024
Paweł Strzałkowski
Ruby
LLM
Embeddings
ChatGPT
Ollama
Handling Errors in Concurrent Ruby, Michał Łęcicki

Handling Errors in Concurrent Ruby

14
.
11
.
2023
Michał Łęcicki
Ruby
Ruby on Rails
Tutorial
Recap of Friendly.rb 2024 conference

Insights and Inspiration from Friendly.rb: A Ruby Conference Recap

02
.
10
.
2024
Kaja Witek
Conferences
Ruby on Rails

Covering indexes - Postgres Stories

14
.
11
.
2023
Jarosław Kowalewski
Ruby on Rails
Postgresql
Backend
Ula Sołogub - SQL Injection in Ruby on Rails

The Deadly Sins in RoR security - SQL Injection

14
.
11
.
2023
Urszula Sołogub
Backend
Ruby on Rails
Software
Michal - Highlights from Ruby Unconf 2024

Highlights from Ruby Unconf 2024

14
.
11
.
2023
Michał Łęcicki
Conferences
Visuality
Cezary Kłos - Optimizing Cloud Infrastructure by $40 000 Annually

Optimizing Cloud Infrastructure by $40 000 Annually

14
.
11
.
2023
Cezary Kłos
Backend
Ruby on Rails

Smooth Concurrent Updates with Hotwire Stimulus

14
.
11
.
2023
Michał Łęcicki
Hotwire
Ruby on Rails
Software
Tutorial

Freelancers vs Software house

02
.
10
.
2024
Michał Krochecki
Visuality
Business

Table partitioning in Rails, part 2 - Postgres Stories

14
.
11
.
2023
Jarosław Kowalewski
Backend
Postgresql
Ruby on Rails

N+1 in Ruby on Rails

14
.
11
.
2023
Katarzyna Melon-Markowska
Ruby on Rails
Ruby
Backend

Turbo Streams and current user

29
.
11
.
2023
Mateusz Bilski
Hotwire
Ruby on Rails
Backend
Frontend

Showing progress of background jobs with Turbo

14
.
11
.
2023
Michał Łęcicki
Ruby on Rails
Ruby
Hotwire
Frontend
Backend

Table partitioning in Rails, part 1 - Postgres Stories

14
.
11
.
2023
Jarosław Kowalewski
Postgresql
Backend
Ruby on Rails

Table partitioning types - Postgres Stories

14
.
11
.
2023
Jarosław Kowalewski
Postgresql
Backend

Indexing partitioned table - Postgres Stories

14
.
11
.
2023
Jarosław Kowalewski
Backend
Postgresql
SQL Views in Ruby on Rails

SQL views in Ruby on Rails

14
.
11
.
2023
Jan Grela
Backend
Ruby
Ruby on Rails
Postgresql
Design your bathroom in React

Design your bathroom in React

14
.
11
.
2023
Bartosz Bazański
Frontend
React
Lazy Attributes in Ruby - Krzysztof Wawer

Lazy attributes in Ruby

14
.
11
.
2023
Krzysztof Wawer
Ruby
Software

Exporting CSV files using COPY - Postgres Stories

14
.
11
.
2023
Jarosław Kowalewski
Postgresql
Ruby
Ruby on Rails
Michał Łęcicki - From Celluloid to Concurrent Ruby

From Celluloid to Concurrent Ruby: Practical Examples Of Multithreading Calls

14
.
11
.
2023
Michał Łęcicki
Backend
Ruby
Ruby on Rails
Software

Super Slide Me - Game Written in React

14
.
11
.
2023
Antoni Smoliński
Frontend
React
Jarek Kowalewski - ILIKE vs LIKE/LOWER - Postgres Stories

ILIKE vs LIKE/LOWER - Postgres Stories

14
.
11
.
2023
Jarosław Kowalewski
Ruby
Ruby on Rails
Postgresql