Monolithic applications are great when you start building your company, but as time progresses, they become difficult to maintain. These codebases, as they grow, easily become Big Balls of Mud.
When building large applications in frameworks like Rails, the very convention-over-configuration design principles that made Rails such a joy to use begin to get in the way when the application grows in scope. You may be experiencing the same pains as well if:
Refactoring is difficult and tedious, because methods and classes depend on too many other classes
You have an ever-growing list of business objects that are difficult to keep in your head. In fact, nobody seems to be able to understand the system as a cohesive whole
Changing code in one area of the code leads to unexpected and unintended side effects in other areas of the code, because it’s easy to call out to global services and objects
A key principle in DDD is that the software you build must closely mirror the (business) domain of the organization that builds it. Thus, we need to do some homework to understand the business domain of your software.
A Domain is what the business does, and the context of how it does it.
Let’s revisit our Delorean example from the prior post. In it, the company is marketed as the Uber for time-travel trips. Thus, its “domain” (the “what it does”) is Time-travel Ridesharing. Also included in the Domain is the “how” of how it does it – by partnering drivers who own time-traveling Delorean vehicles with passengers who want to make time travel trips.
To get at more nuances in the business domain, DDD introduces another concept, called the Subdomain:
A Subdomain represents the smaller groups or units of the business that collaborate in the day-to-day to accomplish the business’ goals.
Delorean is divided up into several teams within the company. Let’s look at two of them, and see what they’re responsible for:
Trip Platform team
Finance Operations team
Design and support the systems that route trips and connect drivers to passengers
Manage the systems that involve financial institutions and credit card processors
Connect passengers to drivers
Route the driver to their next destination
Notify passengers of arriving drivers
Alert drivers to new passengers
Process payouts to drivers
Maintain system-wide transaction history for auditing
Build financial reports
Process credit card charges to passengers
Each of these two groups animate a business responsibility, or subdomain. Let’s name them Ridesharing Experience and Ecommerce, respectively.
Now we’ve got a general illustration of the business and two of its units that help it function in the day-to-day. The Domain and Subdomain are ways to model the problem space of your business – and how it acts to fulfill these roles. Chances are, your business org chart will closely reflect the subdomains of your business. In the real world, the delineations may be less clear – teams may be responsible for multiple, overlapping subdomains.
Let’s fill in this diagram with a few more subdomains in the Delorean business:
Customer Support subdomain: resolving customer support tickets coming in through email
Marketing subdomain: managing marketing email campaigns and marketing coupon codes
Identity subdomain: How the system tracks each user and his/her identifying information
Bounded Contexts in the solution space
This diagram in front of us now reflects the business objectives of the company, divided into logical units that (hopefully) accomplish its goals in the real world. Now we are going to overlay the software systems that accomplish these goals over this diagram. These software systems are described as Bounded Contexts:
A Bounded Context is a system that fulfills the goals of the business in the real world.
Any of our software systems (like a web service or web app) that operate as concrete instances in the Real World are considered Bounded Contexts.
Now it so happens that at Delorean, all these subdomains are implemented in one system – one Big Ball of Mud Rails Monolith. We’ll draw a blue box around the subdomains whose functions are implemented by the software system. In this case, we’ll start with our aforementioned Rails monolith:
Since it’s the monolith, it basically does everything – and so here, it’s eating all the other subdomains in the diagram.
Let’s not forget – we have a few other software systems we haven’t modeled out here. What about all the nice third party integrations that the company uses? These are software systems too. We’ll draw them as blue boxes.
By the way – what we’ve drawn here is a Context Map – a diagram that mixes business objectives and concrete implementations of software systems. It’s useful for assessing the lay of the land of your software systems and visualizing dependencies between teams.
Now, this is reasonable and clean, but we live in the real world, and real world software rarely comes out looking consistent and coherent. If you’ve built your Rails app following its out-of-the-box conventions, your app internally lacks the groupings necessary to visualize your app in its constituent components. In reality, the Delorean codebase looks something more like this:
The point being – Rails does not enforce any organizational constraints on our software systems – meaning that logical business units (our subdomains) that suggest decoupled interfaces – are not materialized in the code, leading to confusion and increasing complexity as the years go by.
The big idea: Organize Rails code into modules by business subdomain
Even though your Ruby classes in your application probably live in the global namespace, they can easily be plucked into modules. Our goal is to create logical groups of domain code that can be isolated into self-contained components.
Indeed, one of the goals of Domain-Driven Designs is to have a one-to-one mapping from a Subdomain to a Bounded Context.
OK, what does this mean? Let’s get into some recommendations, along with examples.
Invert folder structures into a flat domain-oriented grouping
You may recall that following Rails conventions leads us to folder hierarchies that group classes by roles:
Let’s move everything out to a new directory structure: let’s group like functionality by domain, instead. We’ll start with a first variation, which I’ll call a flat domain-oriented grouping.
Next, you’ll want to modulize the classes from what they were before. Since the Driver class falls under the Ridesharing domain, we’ll add it to a Ridesharing module:
You’ll want to do this for every class you move into the app/domains flat directory structure.
Reference associated models by full class name
Additionally, you’ll need to change your ActiveRecord model associations to refer to the class by its full, modulized path:
Keep controllers up to date on where to find their newly modulized views
You’ll also need to insert this small bit to let routes from the controller know where to look for the views:
Here’s the cool thing: You don’t have to move all your code at once. You can pick one little domain in your application, the most mature area of your code or the area which you have the best understanding around, and begin moving its concerns into a single domain folder, all while leaving existing code at rest until it’s ready to move.
Now, we’ve made some small steps to achieving architectural clarity in our application. If we look now, our modular folder structures have helped us grouped our code like so:
Under the hood, our app might look more like this:
What works well with this approach?
There’s less noise in each file directory – by grouping like files by domain-specificity, we find a natural organizational point
The entities that remain in each domain folder are highly cohesive – they most likely naturally tend to communicate with each other and appear naturally with each other
Entities that do not belong together are now separated (looser-coupled)
If you have engineering teams that work along Subdomain-responsibilities, these engineers now can work in a more streamlined, isolated fashion. Looser coupling allows these teams to make changes with confidence that they will not introduce regressions or merge conflicts back to the codebase
The stage is now set in the long run to begin moving each of these domain folders into an independent software service (more on that in a future blog post)
If you want some further guidance into this folder structure, I’ve developed a sample app which exhibits this domain-oriented folder structure: http://github.com/andrewhao/delorean. Take a look and let me know what you think.
What have we learned?
In our time together, we learned about domain-driven design concepts around Domains and Subdomains. We learned how to visualize our software systems as Bounded Contexts on a Context Map, which showed us the areas of the system that belong together as coherent parts.
Ending on a practical note, we illustrated how Rails files and folders could be “inverted” and reimagined as domain-first groupings.
In my next post, we’ll continue our discussion in an upcoming blog post on how to further decouple our domain-oriented Rails code with domain events, and eventually make our way into the land of microservices.
Have you built an app with an unconventional structure like this? How has it worked out for you? Let us know in the comments!
Bring clarity to your monolith with Bounded Contexts