Quantcast
Viewing all articles
Browse latest Browse all 25

Don’t Create Aggregate Roots

Image may be NSFW.
Clik here to view.
roots

My previous post on Domain Events left some questions about how aggregate roots should be created unanswered. It would actually be more accurate to say how aggregate roots should *not* be created. It turns out that this is one of the less intuitive parts of domain-driven design and has been the source of many arguments on the matter. Let’s start with the wrong way:

   1:  using (ISession s = sf.OpenSession())
   2:  using (ITransaction tx = s.BeginTransaction())
   3:  {
   4:      Customer c = new Customer();
   5:      c.Name = "udi dahan";
   6:   
   7:      s.Save(c);
   8:      tx.Commit();
   9:  }

I understand that the code above is representative of how much code is written when using an object-relational mapper. Many would consider this code to follow DDD principles – that Customer is an aggregate root. Unfortunately – that is not the case. The code above is missing the real aggregate root.

There’s also the inevitable question of validation – if the customer object isn’t willing to accept a name with a space in it, should we throw an exception? That would prevent an invalid entity from being saved, which is good. On the other hand, exceptions should be reserved for truly exceptional occurrences. But if we don’t use exceptions, using Domain Events instead, how do we prevent the invalid entity from being saved?

All of these issues are handled auto-magically once we have a true aggregate root.

Always Get An Entity

Let’s start with the technical guidance – always get an entity. At least one. Also, don’t add any objects to the session or unit of work explicitly – rather, have some other already persistent domain entity create the new entity and add it to a collection property.

Looking at the code above, we see that we’re not following the technical guidance.

But the question is, which entity could we possibly get from the database in this case? All we’re doing is adding a customer.

And that’s exactly where the technical guidance leads us to the business analysis that was missing in this scenario…

Business Analysis

Customers don’t just appear out of thin air.

Blindingly obvious – isn’t it.

So why would we technically model our system as if they did? My guess is that we never really thought about it – it wasn’t our job. So here’s the breaking news – if we want to successfully apply DDD we do need to think about it, it is our job.

Going back to the critical business question:

Where do customers come from?

In the real world, they stroll into the store. In our overused e-commerce example, they navigate to our website. New customers that haven’t used our site before don’t have any cookies or anything we can identify them with. They navigate around, browsing, maybe buying something in the end, maybe not.

Yet, the browsing process is interesting in its own right:

  • Which products did they look at?
  • Did they use the search feature?
  • How long did they spend on each page?
  • Did they scroll down to see the reviews?

If and when they do finally buy something, all that history is important and we’d like to maintain a connection to it.

Actually, even before they buy something, what they put in their cart is the interesting piece. The transition from cart to checkout is another interesting piece. Do they actually complete the checkout process, or do they abandon it midway through?

Add to that when we ask/force them to create a user/login in our system.

Are they actually a customer if they haven’t bought anything?

We’re beginning to get an inkling that almost every activity that results in the creation of an entity or storing of additional information can be traced to a transition from a previous business state.

In any transition, the previous state is the aggregate root.

In the beginning…

Let’s start at the very beginning then – someone came to our site. Either they navigated here from some other web page, they clicked on an email link someone sent them, or they typed in our URL. This can be designed as follows:

   1:  using (ISession s = sf.OpenSession())
   2:  using (ITransaction tx = s.BeginTransaction())
   3:  {
   4:     var referrer = s.Get<Referrer>(msg.URL);
   5:     referrer.BroughtVisitorWithIp(msg.IpAddress);
   6:   
   7:     tx.Commit();
   8:  }
   9:   

And our referrer code could look something like this:

   1:  public void BroughtVisitorWithIp(string ipAddress)
   2:  {
   3:     var visitor = new Visitor(ipAddress);
   4:     this.NewVisitors.Add(visitor);
   5:  }
   6:   

This follows the technical guidance we saw at the beginning.

It also allows us to track which referrer is bringing us which visitors, through tracking those visitors as they become shoppers (by putting stuff in their cart), finally seeing which become customers.

We can solve the situation of not having a referrer by implementing the null object pattern which is well supported by all the standard object-relational mappers these days.

How it works internally

When we call a method on a persistent entity retrieved by the object-relational mapper, and the entity modifies its state like when it adds a new entity to one of its collection properties, when the transaction commits, here’s what happens:

The mapper sees that the persistent entity is dirty, specifically, that its collection property was modified, and notices that there is an object in there that isn’t persistent. At that point, the mapper knows to persist the new entity without us ever having to explicitly tell it to do so. This is sometimes known as “persistence by reachability”.

Where validation happens

Let’s consider the relatively trivial rule that says that a user name can’t contain a space.

Also, keep in mind that a registered user is the result of a transition from a visitor.

Here’s *one* way of doing that:

   1:  public class Visitor
   2:  {
   3:     public void Register(string username, string password)
   4:     {
   5:        if (username.Contains(" "))
   6:        {
   7:           DomainEvents.Raise<UsernameCantContainSpace>();
   8:           return;
   9:        }
  10:   
  11:        var user = new User(username, password);
  12:        this.RegisteredUser = u;
  13:     }
  14:  }
  15:   

This actually isn’t representative of most of the rules that will be found in the domain model, but it illustrates a way of preventing an entity from being created without our service layer needing to know anything. All the service layer does is get the visitor object and call the Register method.

Validation of string lengths, data ranges, etc is not domain logic and is best handled elsewhere (and a topic for a different post). The same goes for uniqueness.

Summary

The most important thing to keep in mind is that if your service layer is newing up some entity and saving it – that entity isn’t an aggregate root *in that use case*. As we saw above, in the original creation of the Visitor entity by the Referrer, the visitor class wasn’t the aggregate root. Yet, in the user registration use case, the Visitor entity was the aggregate root.

Aggregate roots aren’t a structural property of the domain model.

And in any case, don’t go saving entities in your service layer – let the domain model manage its own state. The domain model doesn’t need any references to repositories, services, units of work, or anything else to manage its state.

If you do all this, you’ll also be able to harness the technique of fetching strategies to get the best performance out of your domain model by representing your use cases as interfaces on the domain model like IRegisterUsers (implemented by Visitor) and IBringVisitors (implemented by Referrer).

And spending some time on business analysis doesn’t hurt either – unless customers really do fall out of the sky in your world Image may be NSFW.
Clik here to view.
:-)


Viewing all articles
Browse latest Browse all 25

Trending Articles