In our last post we took a closer look into the difference between an application with anemic model and one designed with rules of DDD (Domain Driven Design). In this article we would like to show you how to achieve transition between these models step by step. We will refactor a simple issue tracking application, with typical layer segregation, to one modeled according to domain driven tactical design patterns.
Our issue tracking application is quite simple. There are several business operations you can do using it—all through REST API and all fully covered by integration tests (see tests here). You can:
- create a new issue
- get all issues
- comment on an issue
- change issue status
Some operations have validation rules:
- available transition of status are: new -> in_progress, in_progress -> done
- comment can be added only to issues with status new or in_progress
Our first implementation is a very common one. We have 4 packages responsible for given layers of our application. So we have a controller package with IssueController, where we handle all http requests. There is also a model package with our Issue, which is JPA entity, as well as IssueComment. Finally there is of a course IssueService in service and two repositories related to our entities in a repository package.
Typical roundtrip is quite simple:
- In controller we handle http request, get parameters from url or request body and pass them to service
- Service is the heart of our application—all the logic goes here. We load entity using repository, do some business operation, and return the object (if necessary) after modifications
- Controller retrieves domain object after the call to service and converts it (if any) to json
Example of a service operation can be changing the issue status:
Let’s try to refactor this application to domain driven design.
According to DDD concepts, we need to think about our domain models and its invariants, recognise entities, value objects as well as aggregate roots. Our candidates for entity are Issue and IssueComment, cause these objects are the ones which need to be identifiable in our system. In fact IssueComment doesn’t have to be an entity—we do not use its id and don’t need to distinguish between these objects. We model it as a JPA entity with id just to simplify ORM mapping. So in DDD world Issue becomes the only entity and also becomes the aggregate root—it contains reference to comments, but when modifying we treat them as a single unit.
If we know our aggregate root, then it is easy to start refactoring. All operations which change the state of aggregate needs to be inside it. So we need to move changing status and adding comment methods from service to Issue.
Of course to achieve that we needed to tweak hibernate mapping a little bit. We changed comments field from
We’re using lazy loading and cascading instead of additional repository loading of comments. Thanks to this our aggregate can modify its invariants (fields) without the need to load any additional resources.
Moreover, all available operations on issue are now in Issue class which has at least 3 advantages:
- all validation logic can be now placed inside the object in which the change is happening
- at first glance we see Issue API - which gives us very fast understanding what can be done with issue from business perspective
- no one can introduce an inconsistent state of our object cause no public modifiers (like setStatus) are available
One more benefit, which is not that obvious, is that an operation inside aggregate can only modify its invariants. Comparing to modification of entity in service layer, it can be tempting to put some more and more logic there. Let’s imagine business requirement that a user who comments on an issue starts watching it. No problem, we just inject UserRepository into IssueService, and after adding comment we change the user and save it. In DDD model there is no way to do this - we don’t have any mechanism to load and modify user inside Issue entity. We can only play with its internals.
Refactoring service - simplification
Due to moving of business logic from service to entity, service is now simplified. It does only 3 things:
- Loads the aggregate from repository
- Calls the method on loaded aggregate to modify it
- Saves the modified object
Example of update status method from service is
All the business logic goes to Issue. If there is no additional operation done by service, like sending event or operations on other aggregate root, we can do even more. Get rid of service and do all its logic in the controller.
To be aligned with DDD concept of repository we need to refactor it a little bit. In anemic model we used 2 repositories - one for Issue, second for IssueComment. Repository was created using spring-data repository by creating interface extending CrudRepository. Such repository is a nice solution for no-boilerplate access to database but has some drawbacks.
Firstly, it’s coupled directly to specific implementation. If we would like to change it (for example in tests to use in memory saving), we need to do some mocking or provide some custom bean with all the methods we have in CrudRepository implemented.
Secondly, with spring-data repositories we get bunch of default implementation of methods we do not want, like count, exists or deleteAll.
So we refactored repository to be an interface which has only those methods we would like it to have.
Moreover, as you can see finding an issue is now done using IssueId value object instead of Long. This way we avoid the mistake of providing some different Long from different entity.
Implementation of this interface uses spring data repository underneath, but of course you can easily replace it with anything you want depending on usage context.
Last thing worth mentioning is the total repackaging of our application whilst doing migration from anemic model to ddd. We started from 4 packages which were splitted by layer. In DDD model we have 3 packages: application, domain and infrastructure.
Domain contains our entities and value objects as well as the repository interface (we have also IssueIdSequenceGenerator here but it’s a different story that we will describe in another article). All the business logic belongs here.
Application has the controller and all stuff related to converting from json to model and back. It also contains application service (our IssueService). Application uses domain objects to instrument them (loads aggregates, calls business operations).
The last package is infrastructure which contains implementation of all interfaces, which are used in domain, as well as all classes used internally to provide this implementation (e.g. CrudIssueRepository).
Thanks to such repackaging we have no problem with locating where to put new stuff, which shows up when new business requirements arrive. Question may arise where to put new classes, for example if we would like to introduce users ‘module’. Should we then add new packages in application, domain and infrastructure and put, under each of these packages, current issue ‘module’ inside issue package and create new users module?
Of course not. According to DDD concepts users ‘module’ is a different bounded context so we should create separate module (maven one) or at least create 2 different root packages and it should look like:
Is DDD worth doing?
We just went through migration process from anemic model to DDD and, as you can see, it is not that simple. In a bigger application it can be very difficult or maybe even impossible. Is it worth doing? Of course the answer is: it depends!
DDD is not a silver bullet. For a simple CRUD application or for an application with little business logic it can be an overkill. As soon as your application grows to quite a big one, DDD is worth considering. Pointing once again to the main benefits you can gain using DDD:
better expression of business logic in domain objects through meaningful methods
domain objects enclose transaction boundaries through manipulating only its internals, which simplifies business logic implementation and does not increase your graph of connected domain objects
very straightforward structure of packages
better separation between domain and persistence mechanism
I hope I encouraged you to give DDD a try. Happy DDD-ing;-)