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, onTask
s must be completed in order. It is nonsense to try and complete step 2 on the 4th task in theWaypoint
, 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[]
ofTask
, so again here, theTask
must be completed in order, so there's no need to expose a unique identifier for those either. Knowing that a task is thenᵗʰ
on aWaypoint
is more than adequate for addressing it uniquely.Completing a
Step
, or aTask
changes the state of thatWaypoint
(e.g fromin_progress
tocomplete
, when they're all done). To prevent having to signal up- and down- the hierarchy, the only way to change aWaypoint
state is to make sure theWaypoint
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)