Using anti-requirements to find system boundaries
We all love building greenfield projects. 1 But inevitably, starting a new project involves lots of meetings with business stakeholders to hash out initial requirements and canonical data models. Those are…not so fun.
When one of those meetings occurs after a carb-heavy lunch, it’s easy for your mind to drift away…back to those university lectures about entity design. Think of the nouns and what attributes they have. A dog and a cat are both animals and have 4 legs. Except now it’s Customers, Orders, Products, and Shopping Carts.
Is this the best way to build a system, though? Didn’t we do the exact same thing on the previous greenfield project we’re now rewriting? Surely we won’t make all the same mistakes we made last time…right?
🔗One cart to rule them all
As the meeting continues, the design for the shopping cart begins to take shape. ShoppingCart
is a noun, after all, and it’s got a list of items in it, each of which has simple attributes like Price
and Quantity
. Here’s the shopping cart part of the entity relationship diagram we’ll print out and keep at our desk 2 like a holy article of software design scripture:
We’ve also realized that a cart has some behavior associated with it as well, operations like AddToCart()
, SaveForLater()
, and Checkout()
. So we’re now combining data and behavior together…this is essentially an aggregate which means now we’re doing domain-driven design!
🔗More attributes, more problems
During development, we start to see some flaws in the plan.
First, we learn that if the price of an item goes down, the new lower price should also be reflected in the shopping cart. So whenever a price changes, we must copy that value to any shopping cart containing that item. However, if the price of an item goes up, we need to warn the user about it and make them accept the new price. So now the cart items need to store the current price and the previous price, and we have to do a lot of copying whenever any related data changes.
Next, we realize that we need the inventory level to accurately reflect the available inventory in the warehouse. The business intends to use this to pressure customers to purchase before it’s gone. 3
To keep this value up-to-date, every time the inventory of any item changes in the warehouse, we would need to check every active shopping cart for an instance of that item and update its value. You may be able to join tables to get this information, but that’s not always an option. For example, you might need the data to be denormalized for performance, or the warehouse data might exist on a physically different system that can’t participate in a database join.
It gets worse. As it turns out, we discover similar concerns around delivery estimates, item names, and descriptions. So every time any of these values change, they’ll also need to be copied from their source of truth to any shopping cart with a matching item. At least the marketing folks insist that changes to product names and descriptions should be infrequent and primarily limited to typos. Let’s hope that’s true.
So now our shopping cart starts to look a lot messier, and we’re starting to get worried thinking about all the batch jobs we’ll need to write to keep this thing updated.
Our Cart object no longer looks like a proper DDD aggregate, with everything dependent upon everything else and data being copied everywhere.
The sinking feeling of déjà vu from the old project starts to creep in. What happened? And, more importantly, how can we fix it?
🔗Anti-requirements to the rescue
To help decompose a complex domain, we can use anti-requirements 4 to find attributes incorrectly lumped together on the same entity. Using anti-requirements is a powerful way to increase autonomy by breaking your domain into separate islands that can evolve independently. 5
Anti-requirements are deceptively simple: you create some fake requirement concerning two attributes and present it to business stakeholders. “If the product has more than 20 characters in its name,” you say to them, “then its price must be at least $20.”
When they laugh at you, that’s a hint that although those two attributes are verbally associated with the same noun, there isn’t any meaningful logical relationship between them. 6
Without anti-requirements, teasing out these details can be tricky. Since business domain experts tend to think of this stuff as obvious, which makes them unlikely to volunteer this information. They’re generally surprised that developers don’t know it already. That makes it our job as developers and architects to dig for it.
So with this in mind, let’s go back to our shopping cart and ask ourselves: Will the business people think I’ve lost it if I ask what business rules might operate on Attribute A and Attribute B? If the answer is yes, you’ve likely found an anti-requirement.
🔗A new and improved cart
Let’s start teasing out some anti-requirements and see what effect that has on our shopping cart, beginning with the concept of price.
- When the price of a product exceeds $100, the name should be changed to all caps. Ridiculous!
- When a product description is longer than 3000 characters, the price should be increased by 10%. Ludicrous!
- When the inventory for an item is higher than 1000, we should charge 10% more. Inconceivable!
But wait, we need to be careful. When hearing that last anti-requirement, our business stakeholder might say that while that is indeed inconceivable, it could be possible that we’d need to charge more when inventory is low. After all, that’s just supply and demand in action. By using anti-requirements in this way, you might accidentally discover business requirements that could have been overlooked otherwise.
But whatever anti-requirements we dream up, it remains clear that price and quantity are related. After all, you must multiply price
× quantity
to get the total cost.
This suggests that the highly-coupled price and quantity values could be extracted elsewhere.
In the same way, we can start to analyze other pairs of attributes, crafting anti-requirements for each and using how ridiculous they sound to determine whether to extract other groups of attributes that are more tightly coupled.
- The name of a product affects estimated delivery because we ship products alphabetically. Absurd!
- We must update the description of an item every time the inventory level changes. Preposterous!
- The more inventory we have of an item, the longer it will take to ship them. Wackadoodle! 7 8
Remember that shopping cart entity? We used anti-requirements as a club to bash it into pieces. It turns out that while a shopping cart is a noun used by the business, there is no “cart” anymore…only a simple CartId
rather than a full-blown entity or aggregate.
Eagle-eyed readers will notice here that the Quantity
is not owned by any one thing but is shared between Sales, Shipping, and Warehouse. It’s important to realize that even single attributes don’t always mean the same thing. In Sales, quantity is a multiplier for the price. In Shipping, it’s how many items to put in a box…or even multiple boxes. In Warehouse, it’s how many things to reserve and restock. The values just happen to come from the same place, and we’ll show how to handle that a little later.
This shows that not all the nouns the business uses need to have a corresponding entity in your domain model.
🔗Improved efficiency
Only grouping together data that changes together has a lot of technical and organizational advantages as well.
From a technical perspective, attributes that change together should also be cached similarly. For example, a product’s name and description do not change frequently and can be cached for a long time, but price and inventory could change frequently. Storing them in different entities allows us to use the most appropriate caching strategy for each. In our case, storing the product name and description in a JSON file hosted on a content delivery network (CDN) might be a better and more scalable approach than using a relational database.
In fact, if you’re storing product images on a CDN based on a convention like https://mycdn.com/products/{ProductID}/{Size}.png
, then you’ve already begun decomposing your domain using these strategies.
From an organizational perspective, you no longer need to get all the business stakeholders together simultaneously. 9 If you need to add the capability to deliver digital goods that don’t require shipping, the number of people that need to be involved is now significantly reduced to only the people with insights into the relevant parts of the “cart.”
The only remaining problem for our shopping cart is taking all of Humpty Dumpty’s pieces and putting them back together again.
🔗ViewModel composition
Everything has a cost, and decomposing using anti-requirements is no different. Each component gains greater autonomy, but it can feel (at first) that this comes with a price of greater complexity.
Our users still think of a “shopping cart” as a thing and expect to see all the attributes we’ve separated on a shopping cart page together.
We can integrate all the shopping cart attributes on the same page using a strategy called ViewModel composition using tools like ServiceComposer where independent components provided by services or microservices 10 can query their own data from disparate back-end systems and combine it into a single ViewModel without reintroducing coupling at the UI layer.
In ViewModel composition, each component registers its interest in providing data for specific URI route patterns. Then, for each web request, all the interested data providers are asked to fetch their data, which is added to a dynamic ViewModel. Finally, a separate service (let’s call it Branding) takes the ViewModel and renders it to HTML.
The story is similar for POST requests. Components register handlers for POST routes to communicate back to their respective back-end systems, usually with async messages. This is how each service persists the Quantity
value back to its own data store without any knowledge of the other components.
Using ViewModel composition does add some complexity, at least in the short term. It will not make building the first screen faster, but it will make building the 10th, 50th, and 100th screen faster. Limiting unnecessary coupling, even in the UI layer, makes it easier to continue creating new features well into the future.
ViewModel composition techniques also allow flexibility in terms of data storage technology. For example, one service could be powered by a traditional relational database. At the same time, another could use a graph database or key-value store, a heavy caching layer with Redis, or even JSON files on a CDN.
And when a single service only has to worry about its own vertical slice 11 of the overall system, you’ll find that a database diagram actually can fit comfortably on a single sheet of paper.
There’s much more to say about ViewModel composition than can be covered in a single blog post. Check out Mauro Servienti’s blog series on ViewModel composition or his webinar All our aggregates are wrong 12 to learn more.
🔗Summary
In today’s increasingly complex software systems, the “noun has an attribute” approach to modeling is bound to result in classes, components, and systems that become a mess of coupling. Anti-requirements are one strategy we can use to find our logical service boundaries, helping us to discover which attributes belong together and which have no business being anywhere near each other.
Over time, too much coupling causes the system to evolve into a big ball of mud. Eventually, making changes anywhere without breaking something seemingly unrelated becomes impossible.
Organizations that decouple into autonomous services will be able to be more nimble and deliver value to the business years into the future. After all, unlike most other business projects, software isn’t ever really “done”.
Everyone else will be stuck rewriting the system in 3 years. Again.
Or a rewrite of the greenfield project from 5 years ago, but that's another story.
We'll know the project has hit the big time when we can no longer make the diagram fit on one page and remain readable. That's OK. I hear the copy shop down the street has a large format printer we can use…
Spoiler alert: sometimes, these inventory numbers are totally fake and designed to scam you into feeling more pressure to buy! In other cases, you can continue to sell even when there's negative inventory by putting an item on backorder and notifying the customer when it becomes available. Here, let's pretend that we need this inventory to be accurate, at least as much as eventual consistency will allow.
Anti-requirements or antirequirements, with or without the dash. Or, you can call them de-requirements or even dequirements? We'll stick with anti-requirements in this post.
For another example of this kind of decomposition, check out Putting your events on a diet.
You don't always have to ask them about anti-requirements directly. Using anti-requirements as a mental device on your own is just as valuable.
Never ever had so much fun with a thesaurus. 😉
Be careful with this anti-requirement too. In some contexts, inventory and shipping time could be related if a predictive algorithm determines that items with low inventory are likely to oversell, which means we can't promise 24-hour delivery. As always, it depends, so ask the business stakeholders to be absolutely sure.
Harvard Business Review says the [most productive meetings have fewer than 8 people] because the number of people attending tends to have an inverse relationship with meeting productivity. And honestly, even 8 feels like a lot if you want to accomplish anything!
Both terms have been abused so much they're almost meaningless, so take your pick.
In vertical slice architecture, each service is responsible for all of the traditional "layers" (i.e., UI, Application, Domain Logic, and Database) within the slice that can be defined, in part, using anti-requirements. Check out videos on vertical slice architecture by Jimmy Bogard or Derek Comartin for more.
This video uses the same shopping cart example as this article. Remember that the ViewModel composition code shown in the video is already out-of-date. Using ServiceComposer is a lot more user-friendly.