Check out the video of this talk from ElixirConf 2017 below
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:
In our last chat together, we discussed developing a Ubiquitous Language together with the business experts and your development team to help your team work more closely together. Today, we are going to build on that by introducing new Domain-Driven Design (DDD) tools. Then, we’ll introduce a new folder structure for your Rails apps, preparing them for a future in which your application is less coupled and more cohesive. Let’s get started!
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 | |
---|---|---|
Mission | Design and support the systems that route trips and connect drivers to passengers | Manage the systems that involve financial institutions and credit card processors |
Responsibilities |
|
|
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:
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.
Technically speaking, the Bounded Context in DDD-speak is a specific boundary within your domain that your Glossary from your Ubiquitous Language can only apply – the idea being that different Subdomains may have competing or conflicting definitions of terms. This post won’t elaborate on the linguistic nuances of the Bounded Context. For further reading, see Martin Fowler’s explanation on 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.
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.
You may recall that following Rails conventions leads us to folder hierarchies that group classes by roles:
app/ | |
models/ | |
driver.rb | |
controllers/ | |
driver_controller.rb | |
views/ | |
drivers/ | |
show.html.haml |
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.
app/ | |
domains/ | |
ridesharing/ | |
driver.rb | |
driver_controller.rb | |
drivers/ | |
show.html.haml |
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:
# Before: | |
class Driver < ActiveRecord::Base | |
end | |
# After: | |
module Ridesharing | |
class Driver < ActiveRecord::Base | |
end | |
end |
You’ll want to do this for every class you move into the app/domains
flat directory structure.
Additionally, you’ll need to change your ActiveRecord model associations to refer to the class by its full, modulized path:
# Before | |
module Ridesharing | |
class Vehicle | |
belongs_to :driver | |
end | |
end | |
# After: | |
module Ridesharing | |
class Vehicle | |
belongs_to :driver, class_name: "Ridesharing::Driver" | |
end | |
end |
You’ll also need to insert this small bit to let routes from the controller know where to look for the views:
module Ridesharing | |
class DriverController | |
append_view_path(‘app/domains’) | |
end | |
end |
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:
app/ | |
domains/ | |
ridesharing/ | |
vehicle.rb | |
vehicles_controller.rb | |
trip.rb | |
service_tier.rb | |
trip_price.rb | |
… | |
marketing/ | |
campaign.rb | |
campaigns_controller.rb | |
contact.rb | |
… | |
customer_support/ | |
issue.rb | |
issues_controller.rb | |
… | |
identity/ | |
user.rb | |
users_controller.rb | |
role.rb | |
session.rb | |
… | |
ecommerce/ | |
payment.rb | |
payments_controller.rb | |
charge.rb | |
invoice.rb | |
pricing_tier.rb | |
… |
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.
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!
Andrew is a design-minded developer who loves making applications that matter.