DDD for Rails Developers. Part 1: Layered Architecture.
What is DDD
There are many kinds of complexity that you have to deal with developing software and different kinds of applications will have very different sets of problems you need to solve. If you are building the next Twitter, scalability and fault-tolerance are the problems you are probably fighting. On the other hand, these problems are almost never an issue when working on enterprise applications. The complex domain is what you tackle when developing enterprise software. Business processes of a lot of companies are far from being trivial. Thus, refining the domain that provides loose coupling and will be flexible and maintainable in the future is extremely hard and requires a lot of practice and knowledge.
The Book
Eric Evans – the author of Domain Driven Design – coined the set of practices and terminology helping in tackling domain complexity. His book is a must read for every developer working on enterprise applications and I highly recommend it.
DDD and Rails
Working more and more on large rails applications I’ve noticed that in many ways DDD and Rails contradict each other. Therefore, I’ve decided to write a short series of articles, which will be my attempt to reconcile both paradigms and to find a way to use DDD while not fighting Rails.
Before I start, I’d like to mention that I’m going to write about introducing DDD concepts to an existing application. Therefore, despite that Uncle Bob’s approach (check out this awesome talk) may look appealing, introducing it to an existing Rails application with hundreds of thousands lines of code is, probably, the last thing I want to do. Hence, everything I’m going to write about here is, in some way, a compromise.
Layered Architecture
One of the core concepts of Domain Driven Design is the layered architecture. Let’s take a look at what it is, what kind of benefits it brings, and how a typical Rails application violates this fundamental concept.
First of all, the term “Layered Architecture” means that you partition an application into layers:
- User Interface. Responsible for showing information to the user and processing the user’s input.
- Application Layer. This layer is supposed to be thin and it should not contain any domain logic. It can have functionality that is valuable for the business but is not “domain” specific. This includes generating reports, sending email notifications etc.
- Domain Layer. Responsible for describing business processes. Abstract domain concepts (including entities, business rules) must be contained in this layer. In contrast, persistence, message sending do not belong here.
- Infrastructure Layer. Responsible for persistence, messaging, email delivery etc.
The most important idea behind this architecture is that every layer should depend only on the layers beneath it. Thus, all dependencies have the same direction. For instance, the domain layer might depend on some pieces of infrastructure but not the other way around.
Layered Architecture and Rails
Now let’s take a look at the most common violations of the layered architecture I see in typical Rails applications:
- Domain objects serialize themselves into JSON or XML. In my opinion, there is no big difference between representing an object as a piece of html and representing it as lf a piece of JSON. Both are meant to be consumed by external systems, both are parts of the UI layer. So every time you override the
as_json
method you violate the core idea of the layered architecture – you change the direction of your dependencies between layers. Your domain objects start being aware of the UI. - Controllers contain big chunks of business logic. Usually, it’s a few calls to the domain layer and then persisting changes using low-level methods such as
update_attributes
. So next time you go and callupdate_attributes
inside a controller, stop and think again: most likely you are doing it wrong. Thankfully, most Rails developers have already realized that this kind of controllers hard to maintain. I believe, there is no excuse for doing it – even for small applications. - Domain objects perform all sorts of infrastructure related tasks. As a community we agreed not to write fat controllers, but we hit another problem – “Swiss army knife” domain objects. Such objects, besides describing the business, can also connect to a remote service, generate a pdf or send an email. If you come across such an object it needs to be split into at least two: one that is responsible for the domain knowledge and others for performing infrastructure related tasks.
- Domain objects know too much about the database. If you extend your domain objects from ActiveRecord you couple the domain layer to the infrastructure layer. Your domain objects play two roles at the same time. This kind of coupling makes them almost impossible to test in isolation.
Most of these problems can be fixed. Let’s take a look at all of them to see what can be done.
Make a Separate Class for JSON Serialization
You should never override as_json
or other similar methods in your domain classes. Remember, that the responsibility of the domain layer is to reflect the business concepts. I have not worked on a domain where JSON was an important concept. Therefore, if you are not writing a JSON parser, move all JSON related stuff out of your domain.
Imagine a controller having an action that looks like this:
[gist id=”1974980″]
If you need to customize the JSON serialization of the Person class, don’t do it inside the Person class. Create a separate module (e.g. PersonJsonSerializer, PersonJsonifier) that will be responsible for it.
[gist id=”1974986″]
Now, your controller will look like this:
[gist id=”1974993″]
What have we achieved by moving the JSON serialization to a separate class?
- Our domain remains abstract without any knowledge about the UI.
- We split two responsibilities: being a person and serializing it to a JSON. If we didn’t do it, the Single Resposibility Principle would have been violated.
- In addition, we made our controller easier to test. You don’t even need a real person object to test the serializer. Just stub it out. Also, you don’t need to check two branches of that if statement in your functional tests. You can just stub the serializer out. So instead of two tests, we have only one.
Controllers Don’t Contain Any Logic
In a nutshell, controllers should not contain any logic apart from parsing the user’s input or rendering proper templates. If you have a piece of business logic inside a controller move it to the domain layer. There is a misunderstanding that the domain layer consists of only persisted objects. And when you have a complex operations involving several objects you need to orchestrate it inside a controller. This is just wrong. If none of your entities seems to be a good place for this functionality, create a service class (or a module) and put it there.
Imagine, we have such an action:
[gist id=”1975012″]
A better way to do it:
[gist id=”1975016″]
We’ve achieved:
- The controller is easier to test. When before we would have to have two tests, after our change is done we need only one.
- In addition to making our controller easier to test, we’ve made the business logic part explicit and also easier to test.
Domain Objects Should Not Know Anything About Infrastructure Underneath
In short, the domain layer should be abstract. This means that all dependencies on any kind of external services don’t belong there. Imagine that we are developing a blogging engine and one of the requirements is to send a tweet each time a post is published. What is considered to be an “OK” practice is to handle it in the after_create
hook:
[gist id=”1975022″]
Even though it’s only a few lines of code and doesn’t look like much, it is a big deal. Firstly, you’ll have to stub out the Twitter service in all unit tests, which is not what you usually want to test. Secondly, it violates the Single Responsibility Principle, as storing information about a post is not the same as sending notifications to Twitter. Sending this kind of notifications is rather a side effect, an additional service, which is not a part of the core domain. Finally, Twitter can be unavailable and accessing it synchronously is not a good idea anyway.
There are several ways of decoupling our model from infrastructure. One of them is to observe the Post class:
[gist id=”1975026″]
Even better way of doing it is moving all the responsibilities of generating tweets from the observer to the TwitterService class:
[gist id=”2007956″]
In summary, extracting this responsibility from the Post class has helped us to achieve the following:
- It’s easier to test the Post class as you don’t need to stub Twitter out in every test.
- Testing the TwitterService class is also straightforward. We even don’t need an instance of Post to do it.
- We made the domain boundaries of our application explicit. We have the core domain and we have Twitter integration: two completely different domains and, as a result, two classes separated from each other by an observer.
- Having an object responsible for integration with Twitter allows us to change its functionality (for instance, make it asynchronous) without touching the core domain.
Isolate ActiveRecord
I left the hardest problem to the end: coupling with ActiveRecord. Since the domain should be abstract the database schema should not affect how we design our entities. However, we all live in the real world and that’s why it never happens. The following things you should consider developing your domain: what objects are transient, if it’s easy to map the graph of objects into your relation schema, performance etc. Taking all these properties into account is important. However, you should not design your domain in a way that it’s impossible to test it without using the database.
The DDD approach is to extract a separate layer responsible for persistence. For instance:
[gist id=”1975036″]
Having a separate object responsible for persistence simplifies testing a lot and makes it possible to provide alternative implementations. For instance, it’s a common practice to implement SQLPostsRepository and InMemoryPostsRepository. Therefore, you can use the first once for your integration tests and the second one for your unit tests. When your domain is not coupled to ActiveRecord, implementing repositories is a way to go. However, it won’t give you much when all your domain objects extend ActiveRecord::Base. Thus, I use a compromise variant of the repository pattern: put all persistence related methods into a separate module and just extend it.
[gist id=”2009155″]
Having a separate module comprising all the logic related to persistence benefits us in the following ways:
- It provides separation of concerns. Post is a business object and PostsRepository is responsible for persistence.
- It makes mocking the persistent layer straightforward.
- Providing an in-memory implementation of Repository becomes straightforward too: Post.extend InMemoryPostsRepository
Summary
Tackling domain complexity is hard. The bigger your application grows the harder it gets. Jamming everything together works fine for small apps but breaks apart when you have to deal with large applications. Just applying the principles of the layered architecture can help a lot.
However, there is much more in Domain Driven Design that just partitioning your application into layers: entities and values, services and factories, aggregate roots, domain boundaries, anticorruption layers and more. Understanding these concepts and principles and applying them can be really useful for all Rails developers. I’m going to continue writing on these topics.