Practical .NET

Delivering Scalable, Maintainable Objects with Domain-Driven Design

If you've been creating Data Transfer Objects that integrate several objects into one, then you've created what domain-driven design (DDD) calls an aggregate. But there are some rules you need to follow if you want to reap the benefits that DDD promises toward performance, maintainability and scalability.

If you create ASP.NET MVC applications you've probably gotten into the habit of creating a "model class" that holds all the data that a View needs. In other kinds of applications, these classes are referred to as Data Transfer Objects (DTOs). In ASP.NET MVC developers typically pass these DTOs to Views in one method and then get the DTO back through one of the parameters of another method that handles the View's updates. Here are the typical ASP.NET MVC action methods that implement this pattern (server-side developers will recognize the code as a pattern they typically use when building objects that pull together a variety of data to be updated and the returned by a client):

Public Function UpdateSalesOrder(so As SalesOrder) As ActionResult
  ...code to assemble the SalesOrder...
  Return View("UpdateOrder", so)
End Function

<HttpPost>
Public Function UpdateSalesOrder(so As SalesOrder) As ActionResult
  ...code to use the SalesOrder to update the database...
  Return View("UpdateOrder", so)
End Function

In many ways a DTO used this way mimics what DDD calls an aggregate: a single object that encompasses several other objects and contains all the content for a single transaction. However, DDD also has some rules for creating aggregates that are designed to keep your applications simple, maintainable, responsive and scalable. As I discussed in an earlier column, these are the design goals for avoiding the CRAP cycle (Create/Repair/Abandon/Replace) that leads to unmaintainable applications that have to be replaced rather than enhanced.

For my example in this column, I'll use SalesOrder object used in a company's Billing system (or "domain" in DDD-talk) to calculate a sales order's price. In this column, I'm going to start filling in the details of that SalesOrder object in a way that meets the "rules of DDD aggregates."

Adding More Objects
First, in a real application my SalesOrder would, like a DTO, include a number of related objects: an object representing the Customer associated with the SalesOrder, a collection of SalesOrderDetail objects representing the items purchased with the SalesOrder, and a set of SalesOrderAdjustments objects that represent discounts (or mark-ups) to be applied to that SalesOrder. Taking that into account, my SalesOrder class might look something like this:

Public Class SalesOrder
  Public Property Id As String
  Public Property CustomerOwner As Customer
  Public Property Details As List(of SalesOrderDetails)
  Public Property Adjustments As List(of SalesOrderAdjustments)
End Class

In DDD speak, the SalesOrder object is the root of an aggregate made up of Customer, SalesOrderDetail and SalesOrderAdjustment objects. An application would only have to refer to the SalesOrder object to work with all the objects required for pricing.

Not every collection of objects is an aggregate, primarily because not every collection has an aggregate root. For example, ASP.NET MVC developers often pass a List of objects to an ASP.NET MVC View. However, a List would not be considered an aggregate because (among other issues) a List doesn't have a single root element.

Defining the Root
The reason that the root element matters is because, in DDD, the root is responsible for ensuring that all components of the aggregate are in a valid state both when the object is first retrieved and after any changes. As an example, one activity for my SalesOrder object would be adding/removing an adjustment. Following DDD principles, my initial SalesOrder design would be a very poor aggregate because a developer could bypass the root to add a new adjustment like this:

Dim so As New SalesOrder("A123")
so.Adjustments.Add(new Adjustment(AdjustmentTypes.CostWaived))

An aggregate that's defined correctly would have the Adjustments property defined as a read-only collection with the root providing methods for adding or removing adjustments. With a SalesOrder defined that way, a developer would add an adjustment using code like this:

Dim so As New SalesOrder("A123")
so.AddAdjustment(AdjustmentTypes.CostWaived)

Now the root element can keep track of when adjustments are added and ensure that all new adjustments are validated.

That kind of control may be awkward to implement for every component in the aggregate. While Adjustments can only be added or removed from a SalesOrder, SalesOrderDetails can be updated. Managing every possible update to a SalesOrderDetail from the root would be awkward.

In this scenario, an alternative is to have the root act as a central point for validating all of the other objects in the aggregate. One way of implementing that would be to create an IsSaveable property on the aggregate root that validates all the other objects in the aggregate and returns True or False, depending on what that validation code finds (the aggregate root might delegate some of the validation work to the SalesOrderDetail object):

Dim so As New SalesOrder("A123")
so.Details(0).Quantity = -1
If so.IsSaveable Then
  ...saving the sales order...
Else
  MessageBox.Show("Sales order not valid")
End If

Not all classes need an IsSaveable property. In DDD, an updateable object is either an aggregate root or a member of exactly one aggregate. Classes that form an aggregate root should have public properties like IsSaveable while classes that aren't used as aggregate roots don't need them (though, they may have them as private properties to used by the aggregate root to delegate validation work to those objects).

An application, after making any updates to a SalesOrder aggregate, would expect to pass the SalesOrder to some repository class method that would handle saving the SalesOrder. If I was responsible for writing a repository method that saves an aggregate, I'd use the IsSaveable method on the root object of the aggregate to determine if I should save the aggregate. In fact, as a designer, I'd be tempted to define an interface that all aggregate roots should implement to ensure that all aggregates have such a property (or, if there was code common to all of my aggregate roots, create a "base aggregate root class" for aggregate roots to inherit from). That interface, taking advantage of the new Visual Basic 14 support for read-only auto-implemented properties, might look something like this:

Public Interface IAggregateRoot
  Public ReadOnly Property IsSaveable As Boolean
End Interface

Controlling the Size
Requiring a class to be an aggregate root or appear in exactly one aggregate can raise some design issues. In the Billing domain (where pricing would occur), SalesOrderDetails would never be used outside of the SalesOrder aggregate, so restricting that class to the SalesOrder aggregate isn't a problem. But the SalesOrderDetail needs a Product object that holds information about the product being purchased. That Product object will be needed in the Billing domain independent of the SalesOrders aggregate.

Which raises the question of what does (and does not) belong in any particular aggregate. In DDD, a more accurate definition of an aggregate is that it bundles together all the objects affected by a transaction. In other words, when it's time to commit the changes for a particular operation, all the affected objects should be part of a single aggregate (though other objects may be referenced).

In fact, in DDD, an aggregate forms a transaction boundary: before and after a transaction, all the data contained within the aggregate should be consistent and up-to-date. The corollary of that definition is that objects outside of the aggregate may not be consistent … at least, not right away. But, in DDD, it's OK if objects outside of the aggregate don't become consistent until after later transactions are completed.

Accepting the distinction between immediate consistency (which applies to a single aggregate) and eventual consistency (everything outside of the aggregate) means that you can keep your aggregates small. It's entirely possible that, after applying an adjustment, the sales order's price will be correct but the accounting system report of that price will still reflect the old price. Some later transaction would need to reconcile the billing and accounting domains.

As another example, what if the customer's billing address changes in the Customer domain? What happens if that change isn't yet reflected in my SalesOrder aggregate when pricing is done? That's too bad … but it might not matter. The customer's billing address need not be consistent until the bill is sent and, because this process is about calculating the cost of the sales order, it might be perfectly OK for the calculation to be done with a different billing address than the newer version. Or having a different address may not be OK: Some of the charges on the sales order could be dependent on the customer's location. In that case, after a customer's address is changed, it might be necessary to recalculate the sales order's price…eventually. Consistency will need to be achieved, just not immediately.

With this rule in place, you can see that the Product needn't be part of the SalesOrder aggregate for the pricing process because the Product aggregate won't be updated as part of pricing a SalesOrder.

There are multiple benefits that result from building aggregates that guarantee only the immediate consistency absolutely required by your business processes. First, of course, the resulting objects are sufficiently simple that implementing and maintaining them is actually achievable by mere mortals and won't lead you deeper into the CRAP cycle.

But you also end up with aggregates that consist of very few updateable objects. Smaller aggregates scale more effectively (because they take up less memory), improve response time (because they load faster), and are less likely to interfere with each other (because any updateable part of the aggregate is probably only being used by one transaction at a time). Of course, you're going to need some mechanism for ensuring "eventual" consistency (something I'll come back to in a later column).

In the meantime, you've probably noticed that I've started referring to "updateable" objects … which implies that there are other objects that aren't updateable. If you did pick up on that, you're noticing a key distinction in DDD: the difference between value and entity objects. For example, in the Billing domain, does the Product class need to be updated? Obviously, somewhere in the business, product classes must be updateable but probably not in the Billing domain. If that's the case, then the Product object can be defined as a "read-only" object in the Billing domain and, as a result, can go back to nestling inside the transaction boundary of the SalesOrder aggregate. This distinction is interesting enough that I'm going to devote a separate column to that discussion, also. But in that column, again, you'll see that the DDD rules are designed to deliver simpler, more maintainable, better performing and more scalable applications that avoid the CRAP cycle.

About the Author

Peter Vogel is a system architect and principal in PH&V Information Services. PH&V provides full-stack consulting from UX design through object modeling to database design. Peter tweets about his VSM columns with the hashtag #vogelarticles. His blog posts on user experience design can be found at http://blog.learningtree.com/tag/ui/.

comments powered by Disqus

Featured

  • Compare New GitHub Copilot Free Plan for Visual Studio/VS Code to Paid Plans

    The free plan restricts the number of completions, chat requests and access to AI models, being suitable for occasional users and small projects.

  • Diving Deep into .NET MAUI

    Ever since someone figured out that fiddling bits results in source code, developers have sought one codebase for all types of apps on all platforms, with Microsoft's latest attempt to further that effort being .NET MAUI.

  • Copilot AI Boosts Abound in New VS Code v1.96

    Microsoft improved on its new "Copilot Edit" functionality in the latest release of Visual Studio Code, v1.96, its open-source based code editor that has become the most popular in the world according to many surveys.

  • AdaBoost Regression Using C#

    Dr. James McCaffrey from Microsoft Research presents a complete end-to-end demonstration of the AdaBoost.R2 algorithm for regression problems (where the goal is to predict a single numeric value). The implementation follows the original source research paper closely, so you can use it as a guide for customization for specific scenarios.

  • Versioning and Documenting ASP.NET Core Services

    Building an API with ASP.NET Core is only half the job. If your API is going to live more than one release cycle, you're going to need to version it. If you have other people building clients for it, you're going to need to document it.

Subscribe on YouTube