Domain-Driven Design: refinement
Model refinement is an important part of Domain-Driven Design. Although this sounds easy at first, it turns out there is a tension between model refinement and ubiquitous language.
The beginning of a project
Let’s imagine a team is asked to automate the deletion of user accounts. At the beginning of the project they gather the following requirements from different people:
Customer: “I no longer use your service. I want you to delete my account and my data now.”
Product manager: “Some customers delete their accounts by mistake. In those cases we’d like customers to be able to recover their account.”
Compliance specialist: “We need to make sure the business can cope with accidental data loss. Please ensure that we can restore from a backup.”
User experience designer: “After customers have deleted their accounts, they shouldn’t be able to use our software any longer.”
Programmer: “When the account deletion API was invoked, all data associated with an account should be removed from the database.”
Auditor: “Under the new GDPR legislation, you must be able to prove that user accounts will be deleted successfully.”
At a first glance, all of these people appear to be talking about the same thing. They use similar words, so we jump to the conclusion that they all agree. But beware: a lot of complexity hides below the surface.
Take a second look at the requirements now. Pay special attention and you’ll be able to spot a couple of potential conflicts:
- Should we delete an account immediately or wait until a grace period has expired?
- Will we or won’t we be able to recover an account after it has been deleted?
- Is account recovery the same as restoring from a backup or is there a difference?
- Do we have to delete all data associated with an account or do we have to keep an audit trail?
These conflicts arise, because each member of the team holds a different model of the deletion process. In order to find a good solution, the team must resolve all these conflicts.
Overcoming the linguistic divide
Put differently, the first obstacle the team must overcome is a linguistic divide. The members of the team must work together to bridge the communication gap; otherwise they won’t be able to establish a shared mental model.
As they work through the various requirements, they get a chance to experiment with different words and phrases.
A: “I get the feeling that you think of deletion as irreversible - is that true?”
B: “If we can not reverse a deletion, maybe we should wait a little before we actually do it.”
A: “Is there some hidden concept that we don’t yet have a word for?”
Over time, the conversation helps the team to discover concepts that were hidden before:
Maybe they have to split the deletion process into pieces like deletion requests, deletion attempts and deletion records.
Maybe they must deactivate accounts at once, but can delete them only after a period of time.
Maybe they need to come up with separate solutions for account recovery and for restoring from a backup.
Consequently, the uncertainty about the requirements is forced into the open. This gives the team a chance to tackle it head on. Eventually, this process leads the team to both, a shared mental model and a common language.
Three techniques to establish a common language
As you can imagine, establishing a common language is quite a bit of work. But luckily, we have already seen three techniques to bring it within reach:
Define a bounded context: Clearly identify a part of the problem you want to solve. Draw a boundary around that part. Declare everything else as context. By deciding what’s in and what’s out, you essentially reduce the problem size. This, in turn, helps to keep the shared mental model consistent.
Grow your model in iterations: Don’t try to solve everything at once. Begin with a simple model that works. Then grow it from there into a more complex model. This gives you the ability to verify your model early on and to correct mistakes.
Establish a ubiquitous language: As you build your model, you’ll probably create different representations of it. In addition to the source code you might have additional artifacts like guides, runbooks or diagrams. By using the same language in all of these artifacts you’ll greatly reduce the cognitive overhead.
Any problem above a certain amount of complexity will require all three of these techniques.
The tension between refinement and ubiquity
Unfortunately though, there’s a tension between two of the techniques. Our desire to grow the model in iterations will clash with our efforts to establish a ubiquitous language.
Why? Because every time we change the model, we must go back and update all of our artifacts.
Domain-Driven Design asks us to constantly experiment with the words and concepts we use in our models. Therefore, we can expect a lot of changes to come our way. This is good, because every change shows us that we’ve learned something. But it is also bad, because it means we must update each and every one of our artifacts many times.
If we fail to keep our artifacts consistent and aligned, we will lose the benefits of a common language. Our artifacts become cluttered with signs of long forgotten worlds. The noise blunts the sharpness of our language, thereby blurring the lines between our concepts.
We will watch a widening gap between our model and our artifacts. Our diagrams will no longer be helpful, our documentation will start to mislead us, and our source code will become incomprehensible.
Picture yourself starring at a piece of source code, trying desperately to understand if there’s a reason why one and the same thing carries two different names or why two different things carry the same name.
As practitioners of Domain-Driven Design, it is our job to prevent this source of confusion. Every time we make a change to our mental model or to our language, we must therefore go back and update all the artifacts.
I know that this sounds like a lot of work. And frankly, it is. But there are ways to reduce the burden. Stay tuned for the next article, in which I’ll share five ways to facilitate change.
PS: The gap between the model we hold in our heads and the model that is expressed in the source code is very close to the original idea of “technical debt”. Technical debt was not meant to refer to software of low quality, which is how it is commonly used today.