DEV Community

Lee Hambley
Lee Hambley

Posted on

Aggregate Roots in action

In Domain Driven Design there is an entire taxonomy of terms for various kinds of objects, unfortunately it can be difficult to really apply DDD in the day to day, but recently an opportunity surfaced in a project I was working on, and I wanted to write it up.

The classic example of "Aggregate" and "Aggregate Root" is something like the line items on an invoice, and the invoice itself.

From a programming point of view, it doesn't make sense to implement changeQuantity or delete on a lineItem directly, if you do this, the invoice or shopping basket loses track of the items inside, there's no obvious place to recalculate taxes, check stock, etc.

Other examples might be that edits to forum comments should be applied to the forum thread individually, and not simply as a direct edit on the individual post. Notification rules, minimum waiting time between edits, anti-spam, and other considerations must be accounted for when planning the data flow.

From a DDD perspective, this isn't always obvious, stakeholders may talk about "changing the number of invoice items" (a hint that invoice and item are hierarchically owned), but in other cases such as the forum it's less clear, I certainly mentally model changing my comment in a forum as an action that instinctively feels like the comment is the recipient.

In contemporary web app frameworks, typically MVC and/or RDBMs it is really, really easy to accidentally to make your "nested" aggregates (line item, and forum posts/comments) public, the scaffolding generators will encourage you to model the CRUD verbs on the aggregate entities themselves, and of course the RDBMS is going to give everything a nice convenient handle in the form of an auto-incrementing primary key.

Defaults matter, and the prolific use of RAD MVC frameworks backed by classical relational databases sets a lot of teams up poorly for doing good domain design.


Enough pontificating, let's look at this example, something from the real world.

The application models logistics for fast food, groceries and retail. Drivers move around and complete Tasks at Waypoints, each task comprises one or more Steps

From a Driver's point of view at any given Waypoint they has a series of Tasks to complete, each of which has a linear flow of Steps to sign-off.

Although virtually all of the application is backed by MySQL, and all of these entities have primary key IDs, we deliberately kept Task.id and Step.id out of the public API in the latest redesign.

Client teams (internal) have questioned this decision, because they have been accustomed historically to being able to completeAgeCheckStep(stepId), completeSignaturePhotoStep(stepId, photoData), but I wanted to lay out some of the reasons why:

  • Steps, on Tasks must be completed in order. It is nonsense to try and complete step 2 on the 4th task in the Waypoint, so any public API that lets you address changes to tasks other than in the next/first position are misleading consumers.

  • The Waypoint contains a [] of Task, so again here, the Task must be completed in order, so there's no need to expose a unique identifier for those either. Knowing that a task is the nᵗʰ on a Waypoint is more than adequate for addressing it uniquely.

  • Completing a Step, or a Task changes the state of that Waypoint (e.g from in_progress to complete, when they're all done). To prevent having to signal up- and down- the hierarchy, the only way to change a Waypoint state is to make sure the Waypoint is the access point (Aggregate Root) for the things it contains.

  • In general this design is cleaner, because we reduce the surface area of the API from five or six verbs over three nouns, to a couple of different verbs over one noun. The complexity is not eliminated, just morphed, but into a more manageable one, we believe.

  • Leaking integer IDs in general is poor form, it exposes private implementation details, and is a (weak) proxy for the volume of business your platform is doing. It also sets you up for enumeration attacks in case someone is able to find a weakness in any part of your security.

Probably this isn't a slam-dunk argument and we expect the continue defending this decision in our API design team, as things like this are distinctly unusual in API design, we have become so accustomed to CRUD verbs over HTTP/REST that discussing domain-oriented RPC over HTTP type APIs feels like an up-hill battle.

Make no mistake though, such decisions, and battling for simpler APIs can pay huge dividends over time.

Top comments (0)