ReHow to create fully encapsulated Domain Models

time to read 4 min | 788 words

A while ago Udi talked about How to create fully encapsulated Domain Models. He gave a great example, but more importantly, he gave some general rules of the thumb about how to create such a system.

Those rules are, if I understand them correctly:

  • Every message should resolve to a single method call on a domain object. The message handler is responsible for setting up the environment to call this method (getting the parameters, mostly).
  • The domain model should not throw exceptions. This is mostly because attempting to handle exceptions often lead to handling too much or too little. It is easier to define all the domain operations as throwing no exceptions, in which case all exceptions are infrastructure exceptions (or bugs).
  • Communication to the outside world is done using events. To decouple the domain from the service layer.

Things that I am not sure about:

  • What are the roles of services using this model. As a simple example, when new user is created, we should send a welcome email. This involves technical services (IEmailSender, ITemplateEngine, etc) and probably a domain service as well (INotifications.SendWelcome) I am not sure where those fit in.
  • Rules - Same thing. How do I handle things like executing a set of rules in a business action?

Anyway, I thought that it would be interesting to see how I would approach Udi's sample using my own style. What I ended up with is this:

public class AddGameToCartMessageHandler : BaseMessageHandler<AddGameToCartMessage>
{
    IRepository<TradeInCart> tradesRepository;
    IRepository<Game> gamesRepository;

    public AddGameToCartMessageHandler(
        IRepository<TradeInCart> tradesRepository,
        IRepository<Game> gamesRepository)
    {
        this.tradesRepository = tradesRepository;
        this.gamesRepository = gamesRepository;
    }

    [Transaction]
    public override void Handle(AddGameToCartMessage m)
    {
        Future<TradeInCart> cart = tradesRepository.GetFuture<TradeInCart>(m.CartId); 
        Future<TradeInCart> g = gamesRepository.GetFuture<Game>(m.GameId);

        SetupErrorHandling();

        cart.Value.Add(g.Value);
    }

    private void SetupErrorHandling()
    {
        Domain.FailureEvents.GameReportedLost = delegate
        {
             Bus.Return((int) ErrorCodes.GameReportedLost);
        };     
        Domain.FailureEvents.CartIsFull = delegate
        {
            Bus.Return((int) ErrorCodes.CartIsFull);
        };

        Domain.FailureEvents.MaxNumberOfSameGamePerCartReached = delegate
        {
            Bus.Return((int) ErrorCodes.MaxNumberOfSameGamePerCartReached);
        };
    }
}

It doesn't look that different, but let us look at the details, shall we?

  • We don't deal with the session. That is duplicate code that can get quite annoying.
  • Transactions are implicit. This saves the whole (who forgot to call tx.Commit() ) again.
  • The use of futures to get both cart and game at the cost of a single call to the DB.
  • Moved error handling to a separate method.
  • We do not use events, this saves us the trouble of having to unregister them (or manage them using weak references)
  • What we can't see is that an attempt from the domain model to call a delegate that wasn't registered would raise an error. I don't believe in ignoring errors.

I am not sure what to do about the transaction when we run into a domain error. Should we assume that the domain is smart enough to revert state changes? I am pretty sure that this is not something that we would like to assume. But, does it make sense to rollback the transaction?  An attempt to add a game that was report lost should probably trigger some alert somewhere, we don't want to roll that back, obviously.

Thoughts?

More posts in "Re" series:

  1. (16 Aug 2022) How Discord supercharges network disks for extreme low latency
  2. (02 Jun 2022) BonsaiDb performance update
  3. (14 Jan 2022) Are You Sure You Want to Use MMAP in Your Database Management System?
  4. (09 Dec 2021) Why IndexedDB is slow and what to use instead
  5. (23 Jun 2021) The performance regression odyssey
  6. (27 Oct 2020) Investigating query performance issue in RavenDB
  7. (27 Dec 2019) Writing a very fast cache service with millions of entries
  8. (26 Dec 2019) Why databases use ordered indexes but programming uses hash tables
  9. (12 Nov 2019) Document-Level Optimistic Concurrency in MongoDB
  10. (25 Oct 2019) RavenDB. Two years of pain and joy
  11. (19 Aug 2019) The Order of the JSON, AKA–irresponsible assumptions and blind spots
  12. (10 Oct 2017) Entity Framework Core performance tuning–Part III
  13. (09 Oct 2017) Different I/O Access Methods for Linux
  14. (06 Oct 2017) Entity Framework Core performance tuning–Part II
  15. (04 Oct 2017) Entity Framework Core performance tuning–part I
  16. (26 Apr 2017) Writing a Time Series Database from Scratch
  17. (28 Jul 2016) Why Uber Engineering Switched from Postgres to MySQL
  18. (15 Jun 2016) Why you can't be a good .NET developer
  19. (12 Nov 2013) Why You Should Never Use MongoDB
  20. (21 Aug 2013) How memory mapped files, filesystems and cloud storage works
  21. (15 Apr 2012) Kiip’s MongoDB’s experience
  22. (18 Oct 2010) Diverse.NET
  23. (10 Apr 2010) NoSQL, meh
  24. (30 Sep 2009) Are you smart enough to do without TDD
  25. (17 Aug 2008) MVC Storefront Part 19
  26. (24 Mar 2008) How to create fully encapsulated Domain Models
  27. (21 Feb 2008) Versioning Issues With Abstract Base Classes and Interfaces
  28. (18 Aug 2007) Saving to Blob
  29. (27 Jul 2007) SSIS - 15 Faults Rebuttal
  30. (29 May 2007) The OR/M Smackdown
  31. (06 Mar 2007) IoC and Average Programmers
  32. (19 Sep 2005) DLinq Mapping