Modelling Aggregates: Invariants vs Corrective Policies

Designing software systems that are aligned with business often leads to compromises and design tradeoffs. It's usually not feasible to model a system in a way that will in 100% reflect the real world.

Understanding business rules is the first step of getting a useful model, but that's not enough. Modelling them as software often leads to discoveries - we realise that it might not be practical, or even possible to enforce some of the rules.

The right thing to do in such a situation is to go back to domain experts and discuss the tradeoffs we are willing to make. That might mean that we will have to give up on implementing a strongly consistent Invariant and replace it with a Corrective Policy.

Invariants vs Corrective Policies

An Invariant business rule is a rule that will always hold, no matter what we try to do in our system. An example could be a credit card system in which we ensure that a customer can't go over its credit card limit. In real life though, it's usually not possible to enforce this in 100% of the cases. It's plausible to say that some transactions will be handled offline (e.g. on the plane or in some locations with bad connectivity).

In such cases a Corrective Policy makes sure that if the business rule is violated then the system knows how to react to it. An example could be an overutilization policy, that would charge extra interest on top of the normal one. That is just one of the examples where people designing software need an input from domain experts to make a decision how to handle violation of business rules.

Enforcing Invariants with Domain-Driven Design Aggregates

One of the DDD patterns that I find to be particularly useful when modelling software systems is an Aggregate. One of the traits defining it, is a “consistency boundary”. What I mean by it: it's a clear definition of which set of rows is being loaded from database and changed together. Any changes to these rows must be either all saved or rejected (using a concurrency control strategy).

What that gives us in return is an ability to enforce the invariants. If we can make sure that an entity (or set of them) is only updated through an Aggregate, then we can make an assertion that it should never be in an incorrect state. That sounds great, but also comes at a cost - the bigger our aggregate, the slower it is to load and greater chance of concurrency conflicts. On the other hand if we make it too small then we will have to pay the price of implementing policies that will eventually bring the system into correct state.

Using business rules to model Aggregates

To put the above bit of theory into perspective I will use an example that together with Nick Tune we use during our DDD Trainings.

The example is an appointment booking system for a small practice. Initial set of requirements that system needs to implement:

  • Allow scheduling of up to 60 ten-minute slots per day per doctor
  • Each slot can be booked only once
  • Patient can book a double visit if two adjacent slots are available
  • Patient can’t book more than 10 slots in any calendar month

If we were to model a system that supports these requirements, we could start with the simplest and smallest aggregate boundary - a single slot. Then periodically we could schedule these slots and allow the patients to book them. Such boundary would make fulfilling the requirement to only allow to book a single slot once very easy. If two people tried to book the slot at the same time our concurrency control mechanism would reject one of the writes and let through the other. The single booking invariant can be enforced.

Going now to the second requirement - allowing a patient to book two adjacent slots. Having a single slot will make this quite complex - if we try to book two slots at the same time there is a chance that one will get booked as expected, and other one will be rejected. That could lead to a confusing user experience so in order to mitigate this problem we would have to have a corrective policy to fix it.

What we can do instead is to make our aggregate boundary wider - extend it to a day. This way every time we will have to fetch all slots from the database, but also will be able to book two of them and save them to the database at once. The tradeoff that we made was to give up some concurrency and increase the size of the aggregate, but we’ve gained a way of enforcing another invariant.

If we look at the third requirement - maximum 10 slots per month - we could try to jump to a similar conclusion and increase the size of the aggregate to the whole month. After all it’s an easy way to enforce invariants, isn't it? Not so fast. Depending on the number of concurrent users and slots it’s possible we might end up with a poor user experience and a lot of conflicts. In such a case we can have a conversation with domain experts and ask how often this rule is likely to be violated.

If it’s unlikely to be happening a lot and domain experts agree with that, we might be able to keep the aggregate size to a day. In addition to that we could implement a corrective policy (a manual or automated process) that will be able to take an appropriate action - e.g. ask member of staff to call the patient and maybe refer to a specialist.

Summary

In this post we’ve discussed the impact of business rules on the implementation of software - we’ve seen how definition of invariants will impact the design of aggregates in the code. On the other hand the limitations of software systems also impact business processes and can result in an implementation of additional corrective policies, that business didn't think about before.

We've designed an aggregate that is capable of enforcing some of the invariants but also required us to make trade offs between simplicity, correctness, concurrency and size. These are all important factors that need to be considered when designing boundaries in our software systems. If you've faced any other tradeoffs or challenges that affected the design of your aggregates, please do share them in comments!