As I was discussing the PoEAA patterns used to model domain logic (i.e. transaction script, table module, domain model), I noticed that people get the impression (albeit wrong impression) that the domain model pattern is best. So, they set out to apply it on everything.
Not Worthy of Domain Model Pattern
Let's get real. The majority of sub-systems are CRUD-based. Only a certain portion of the system requires the domain model implementation pattern. Or, put it in another way, there are parts of the application that just needs forms over data, and some validation logic (e.g. required/mandatory fields, min/max values on numbers, min/max length on text). For these, the domain model is not worth the effort.
For these, perhaps an anemic domain model would fit nicely.
Anemic Domain Model Isn't As Bad As It Sounds
The anemic domain model isn't as bad as it sounds. There, I said it (at least here in my blog post).
But how does it look like?
package com.acme.bc.domain.model; ... @Entity class Person { @Id ... private Long id; private String firstName; private String lastName; // ... // getters and setters } ... interface PersonRepository /* extends CrudRepository<Person, Long> */ { // CRUD methods (e.g. find, find/pagination, update, delete) }
package com.acme.bc.infrastructure.persistence; ... class PersonRepositoryJpa implements PersonRepository { ... }
In the presentation layer, the controllers can have access to the repository. The repository does its job of abstracting persistence details.
package com.acme.bc.interfaces.web; @Controller class PersonsController { private PersonRepository personRepository; public PersonsController(PersonRepository personRepository) {...} // ... }
In this case, having the Person
class exposed to the presentation layer is perfectly all right. The presentation layer can use it directly, since it has a public zero-arguments constructor, getters and setters, which are most likely needed by the view.
And there you have it. A simple CRUD-based application.
Do you still need a service layer? No. Do you still need DTO (data transfer objects)? No. In this simple case of CRUD, you don't need additional services or DTOs.
Yes, the Person
looks like a domain entity. But it does not contain logic, and is simply used to transfer data. So, it's really just a DTO. But this is all right since it does the job of holding the data stored-to and retrieved-from persistence.
Now, if the business logic starts to get more complicated, some entities in the initially anemic domain model can become richer with behavior. And if so, those entities can merit a domain model pattern.
Alternative to Anemic Domain Model
As an alternative to the anemic domain model (discussed above), the classes can be moved out of the domain logic layer and in to the presentation layer. Instead of naming itPersonRepository
, it is now named PersonDao
.
package com.acme.bc.interfaces.web; @Entity class Person {...} @Controller class PersonsController { private PersonDao personDao; public PersonsController(PersonDao personDao) {...} // ... } interface PersonDao /* extends CrudRepository<Person, Long> */ { // CRUD methods (e.g. find, find/pagination, update, delete) }
package com.acme.bc.infrastructure.persistence; class PersonDaoJpa implements PersonDao { ... }
Too Much Layering
I think that it would be an overkill if you have to go through a mandatory application service that does not add value.
package com.acme.bc.interfaces.web; ... @Controller class PersonsController { private PersonService personService; public PersonsController(PersonService personService) {...} // ... }
package com.acme.bc.application; ... @Service class PersonService { private PersonRepository personRepository; public PersonService(PersonRepository personRepository) {...} // expose repository CRUD methods and pass to repository // no value add }
Application Services for Transactions
So, when would application services be appropriate? The application services are responsible for driving workflow and coordinating transaction management (e.g. by use of the declarative transaction management support in Spring).
If you find the simple CRUD application needing to start transactions in the presentation-layer controller, then it might be a good sign to move them into an application service. This usually happens when the controller needs to update more than one entity that does not have a single root. The usual example here is transferring amounts between bank accounts. A transaction is needed to ensure that debit and credit both succeed, or both fail.
package sample.domain.model; ... @Entity class Account {...} ... interface AccountRepository {...}
package sample.interfaces.web; ... @Controller class AccountsController { private AccountRepository accountRepository; ... @Transactional public ... transfer(...) {...} }
If you see this, then it might be a good idea to move this (from the presentation layer) to an application-layer service.
package sample.interfaces.web; ... @Controller class AccountsController { private AccountRepository accountRepository; private TransferService transferService; ... public ... transfer(...) {...} }
package sample.application; ... @Service @Transactional class TransferService { private AccountRepository accountRepository; ... public ... transfer(...) {...} }
package sample.domain.model; ... @Entity class Account {...} ... interface AccountRepository {...}
Domain Model Pattern (only) for Complex Logic
I'll use the double-entry accounting as an example. But I'm sure there are more complex logic that's better suited.
Let's say we model journal entries and accounts as domain entities. The account contains a balance (a monetary amount). But this amount is not something that one would simply set. A journal entry needs to be created. When the journal entry is posted, it will affect the specified accounts. The account will then update its balance.
package ….accounting.domain.model; ... /** Immutable */ @Entity class JournalEntry { // zero-sum items @ElementCollection private Collection<JournalEntryItem> items; ... } ... /** A value object */ @Embeddable class JournalEntryItem {...} ... interface JournalEntryRepository {...} ... @Entity class Account {...} ... interface AccountRepository {...} ... @Entity class AccountTransaction {...} ... interface AccountTransactionRepository {...}
Now, in this case, a naive implementation would have a presentation-layer controller create a journal entry object, and use a repository to save it. And at some point in time (or if auto-posting is used), the corresponding account transactions are created, with account balances updated. All this needs to be rolled into a transaction (i.e. all-or-nothing).
Again, this transaction is ideally moved to an application service.
package ….accounting.application; @Service @Transactional class PostingService {...}
If there's a need to allow the user to browse through journal entries and account transactions, the presentation-layer controller can directly use the corresponding repositories. If the domain entities are not suitable for the view technology (e.g. it doesn't follow JavaBean naming conventions), then the presentation-layer can define DTOs that are suitable for the view. Be careful! Don't change the domain entity just to suit the needs of the presentation-layer.
package ….interfaces.web; @Controller class AccountsController { private AccountRepository accountRepository; private AccountTransactionRepository accountTransactionRepository; private PostingService postingService; ... }
In Closing...
So, there you have it. Hopefully, this post can shed some light on when (and when not) to use domain model pattern.
Upon reading Patterns, Principles, and Practices of Domain-Driven Design (Scott Millett and Nick Tune), I agree with what they wrote on p. 64 (Chapter 5: Domain Model Implementation Patterns):
ReplyDelete"The majority of sub systems are CRUD based, with only the core domain requiring the domain model implementation pattern to ensure clarity or to manage complex logic. What you should not do is try to apply the domain model pattern for everything. Some parts of your application will simply be forms over data and will require just basic validation instead of rich business logic. Trying to model everything and apply object‐oriented practices would be a waste of effort that would be better spent on your core domain. Software development is all about making things simpler, so if you have complex logic, apply the domain model pattern; otherwise, look for a pattern that fits the problem you have, like the anemic domain model or the table module pattern."
Also on p. 86 (Chapter 6: Maintaining the Integrity of Domain Models with Bounded Contexts)
ReplyDelete"Not all bounded contexts need to share the same architectural pattern. If a bounded context contains a supporting or generic domain with a low logic complexity, you might want to favor a more create, read, update, and delete (CRUD) style of development. If, however, the domain logic is sufficiently complex, it’s best to create a rich object-oriented model of the domain. Once bounded contexts are separated you can go a step further and apply different architectural patterns."
Great pushback on over-applying domain models.
ReplyDeleteGreat writing too, super-clear and to the point, it could go straight onto Martin Fowler's blog. What about cross-posting on dev.to for wider visibility? It would make a good companion to this very good set of DDD articles: https://dev.to/peholmst/strategic-domain-driven-design-3e87