Aggregate Roots in action
Tags: ddd, domaindrivendesign, architecture
Published: 2021-01-26 16:23:07

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:

- `Step`s, on `Task`s 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.