Full-blooded domain models

I’m an OO-guy, plain and simple. I like a smart domain-model because it just fits the way I think about software design. It generally also makes it a hell of a lot easier to test your business logic.

So.. why do we see the Anemic domain model so often? In my opinion O/R mapping is a big culprit. Let me explain.

If you use Hibernate or a similar O/R mapper to persist your domain objects, you generally don’t want these objects to “escape” from your business layer since modifying these objects can have all kinds of side effects. Hibernate will automatically try to save changes if you don’t configure it not to. And who hasn’t seen the infamous LazyInitializationException?

These things will often either cause people to go for an anemic domain model, or to expose only DTOs to the UI layer. But DTOs are in fact also a kind of anemic domain model. They tend to lack exactly the kind of validation and internal logic that makes “real” domain objects such a pleasure to use.

My latest thought on the matter is to reverse the situation.

Imagine your basic 3-layer architecture, in this case for a (very simple) bookstore app. The layering pattern says that each layer should offer its own abstractions that get more high-level as you get higher in the layering. So, where do the domain objects belong? Some people draw them to the side of the layering, but that’s just cheating. If you think about it a good domain model contains business logic, so it belongs in the business layer. In fact, I’d like to argue that it’s too high-level for a data layer.

So, let’s start with a very simple domain:

So, a book is written by an author and a user can have a number of books on their personal book-shelf.

So, this maps to 3 domain classes: Book, User and Author. An Author has a set of books, the book has a reference to its Author and a user has a Set of Books but Book has no reference to a User. Then we have several Value Objects for things like ISBN, e-mail and DateOfBirth. Personally I think Value Objects are one of the best ideas to come out of Domain Driven Design. They’re not persistant so you can create them at will, but they do mean you won’t accidentally assign a street address to an ISBN plus they can contain simple sanity checking.

Now I could easily just let Hibernate map these objects to the database for me, but I don’t want to do that for several reasons:

  • They belong to the business layer. The underlying data layer should not have any knowledge of higher-level layers.
  • I want to be able to give these objects to my UI without fear of side effects, since I consider them part of my business interface.

So, instead I give the data layer its own domain model.

I’ve create PO (Persistent Object) classes, most of which map to classes in the domain. Now wait I minute I hear you say… that’s not a domain model, that’s a database model! Well, you’re right… it is. This is my version of the anemic model, but it’s anemic on purpose.

This model doesn’t contain any business logic, since business logic belongs in the business layer. This is the domain of the data layer, so it closely mirrors what the database looks like. It offers a low-level abstraction just one step above plain SQL. I use Hibernate to map these classes to the database, but mostly because that means I can just annotate them and have a schema generated for me. I know exactly what that schema will look like, since my domain is so simple.

So, why do this? Because it enables the services to do exactly what business services should do: use low-level abstractions to provide higher-level abstractions. The complexity is handled in the service, where it should be.

Now… code talks and BS walks… so let me walk you through the implementation :)

The service interface… if the return type of the method makes you blink, check out this post.

public interface BookService {
 
public static enum BookAddFailure {
    UNKNOWN_AUTHOR, /** Author name not found **/
    DUPLICATE_BOOK, /** A book with the same
                     ISBN is already registered
                     for this author **/
    INVALID_ISBN /** The ISBN is not valid **/
}
 
/**
* Adds a new book to the system and registers it with the correct Author.
*
* @param title
* @param author
* @param edition
* @param price
* @param isbn
* @return
*/
OperationResult<Book, BookAddFailure> addNewBook(
    String title, String author,
    String edition,
    int price, String isbn );
}

And the implementation of the addNewBook() method:

public OperationResult<Book, BookAddFailure> addNewBook(
    String title, String authorName, 
    String edition, int price, String isbn) {
 
    Author author = this.authorService.findByUniqueName(authorName, true);
 
    if ( author == null ) {
        return new OperationResult<Book, BookService.BookAddFailure>(
            Result.FAILURE, null, BookAddFailure.UNKNOWN_AUTHOR );
    }
 
    ISBN newISBN = new ISBN(isbn);
 
   if ( ! newISBN.isValid() ) {
        return new OperationResult<Book, BookService.BookAddFailure>(
            Result.FAILURE, null, BookAddFailure.INVALID_ISBN );
    }
 
    for ( Book book: author.getAllBooksWritten() ) {
        if ( book.getIsbn().equals(newISBN) ) {
            return new OperationResult<Book, BookService.BookAddFailure>(
                Result.FAILURE, null,
                BookAddFailure.DUPLICATE_BOOK );
        }
    }
 
    Book newBook = new Book( author, new Title(title), newISBN );
    newBook.setEdition(edition);
    newBook.setPrice(price);
 
    bookDao.save( convert(newBook) );
 
    return new OperationResult<Book, BookService.BookAddFailure>(
        Result.SUCCESS, newBook, null );
}

Now the sneaky bit of code is in this line:

bookDao.save( convert(newBook) );

The convert() method is a static import from a Conversions class which maps back and forth between domain objects and PO objects. The only thing it does is copy the proper fields, initializing Value Objects when needed.

If we look at another piece of code, this time from the AuthorServiceImpl:

 
public Author findByUniqueName(String uniqueName, boolean includeBooks) {
 
    AuthorPO authorPO = this.authorDao.findByUniqueName(uniqueName);
 
    Author author = convert( authorPO );
 
    if ( author != null && includeBooks ) {
        for ( BookPO po: this.bookDao.findByAuthor(authorPO)) {
            convert(po, author);
        }
    }
 
    return author;
}

we can see the same thing at work. So, we make the mapping an explicit part of the business services, but not of the business domain. It also makes it explicit how much domain you want.

This is the cleanest way I’ve been able to think of to balance smart domain objects with transactional services and predictable persistency. It means you speak to the business layer in terms of the business domain and to the database layer in terms of the database domain. It does mean extra work because of the mapping between the two domains, but personally I prefer my complexity out in the open as opposed to hidden.

There is one aspect I haven’t mentioned yet: complex queries. The PO domain is an abstraction on top of the database, so it means that its objects map to database concepts. This doesn’t mean that each object has to map to a table though. An PO object can just as easily match to the result of a complex query which joins together several tables. This PO can then in turn be used to initialize domain objects without the business domain ever knowing which PO objects map to a table and which don’t.

As it should be in a properly layered system.

One comment on “Full-blooded domain models

  1. You get a lot of respect from me for writing these helpful aritlces.

Leave a Reply

  • Google ads