Domain model refactoring: replace query with composition
Here’s a snippet of ubiquitous language (altered slightly to protect the innocent) from a system I’ve been working on over the past few months:
An Officer is a role played by certain Employees. Each Officer is required to be proficient in a number of Competencies, according to [among other things] what District they’re stationed in.
We originally implement the Officer-District association as a query because it comes from a different bounded context (Rostering) and changes frequently as Employees move around.
When creating an Officer role for an Employee, we simply queried to find out what District they were working in and what Competencies are required to be practiced there. It looked something like this:
public class OfficerRoleFactory : IRoleFactory<Officer>{ ... public Officer CreateRoleFor(Employee employee) { District district = districtResolver.GetCurrentLocationOf(employee); var requiredCompetencies = competencyRepository .GetCompetenciesRequiredToBePracticedIn(district); return new Officer(employee, requiredCompetencies); }}
That was great for when someone first became an Officer, but presented a big problem when they want to move to a different District. To update their required Competencies we have to:
- Find what Competencies were required because of their old District
- Find what Competencies are required in their new District
- Add new required Competencies to the Officer and remove any that no longer apply
Our model did not easily permit this because it failed to encapsulate what District an Officer was working in when their required Competencies were assigned (we simply queried for their current location whenever it was needed). Our code got stuck:
public class Officer : IRole{ ... /// <summary> /// Change the District the Officer is working in. Removes any /// Competencies no longer required to be practiced and adds /// new ones. /// </summary> public void ChangeDistrict(District newDistrict) { var oldCompetencies = competencyRepository .GetCompetenciesRequiredToBePracticedIn(/* what goes here? */); var newCompetencies = competencyRepository .GetCompetenciesRequiredToBePracticedIn(newDistrict); this.requiredCompetencies.Remove(oldCompetencies); this.requiredCompetencies.Add(newCompetencies); }}
An Officer’s old District simply wasn’t defined anywhere.
Make everything explicit
Even without updating Competencies to reflect staff movements, we foresaw a lot of confusion for users between where Training thinks you are and where the Rostering says you actually are.
We decided the best way to resolve these issues was to make the Officer-District association a direct property of the Officer that gets persisted within the Training BC:
It seems like a really simple conclusion now, but took us a while to arrive at because our heads were stuck in the rest of the system where pretty much everything (legacy dataset-driven code) queries back to the live Roster tables. Instead we should have been focusing on domain driven design’s goal of eliminating confusion like this by making implicit concepts explicit:
public class Officer : IRole{ /// <summary> /// The District the Officer is currently stationed in. He/she must /// be proficient in Competencies required there. /// </summary> public District District { get; set; } ... /// <summary> /// Change the District the Officer is working in. Removes any /// Competencies no longer required to be practiced and adds /// new ones. /// </summary> public void ChangeDistrict(District newDistrict) { var oldCompetencies = competencyRepository .GetCompetenciesRequiredToBePracticedIn(this.District); var newCompetencies = competencyRepository .GetCompetenciesRequiredToBePracticedIn(newDistrict); this.requiredCompetencies.Remove(oldCompetencies); this.requiredCompetencies.Add(newCompetencies); this.District = newDistrict; }}
Benefits
Refactoring away from the query to simple object composition makes our domain model a lot easier to understand and also improved some SOA concerns and separation between BCs:
- ‘Where the Training BC thinks you are’ is now an explicit and observable concept. This clears up a lot of confusion both for users wondering why certain Competencies are assigned to them, and developers trying to debug it.
- It breaks an ugly runtime dependency between the Training and Rostering BCs. Previously, if the DistrictResolver failed for some reason, it would block the Training BC from succeeding because it was called in-line. Now we can take that whole Rostering BC offline and Training won’t notice because it knows for itself where each Officer is stationed.
- It allows us to deal with staff movements in a much more event-driven manner. Instead of the DistrictResolver returning up-to-the-second results each time, the District is now an explicit property of the Officer aggregate root that only changes when we want it to — e.g. in response to a StaffChangedDistrict domain event. We can now queue these events and achieve better performance via eventual-consistency.
Overall I am very happy with this.