Domain Driven Design: aggregates implementation
Table of Contents
Aggregates are guards for business principles in domain implementation. They merge several contexts into one, transparent object which unifies the interface for aggregated stuff. For example we can have many entities and value objects nested in aggregate. Our security guy will provide a mechanism for checking aggregated data consistency and integrity. Using policies and specifications (which I described in the previous chapter of the series Domain Driven Design: domain building blocks) we can ensure immutability of business assumptions.
Let’s consider an example
In the previous chapter I mentioned an example from our imagined Vet Helping System and it’s scheduling module. Let’s stay for a while with that concept and try to build something bigger. Last time I said that we could have some appointment rules for appointment’s time, length, patient etc. My suggestion was to create an Appointment aggregate which will collect data about patient and meeting details and be some kind of a guardian for rules.
First of all we should consider what rules our aggregate needs to check. For sure we cannot allow to make an appointment out of vet working hours or allow to schedule two meetings at the same time. I think we should also check that meetings are not in the past and pass minimum and maximum appointment’s time values. So from these rules we can distinguish two information which our aggregate needs to contain: appointment’s start date-time and appointment’s length.
Having information about the appointment’s time is not enough, we need to have knowledge about the patient connected with the meeting. In the case of patients (animals) we should consider checking the breed of the pet. For example, our client (the vet) helps only small animals like hamsters, rats, dogs or cats, because of the small working area in the office. He suppose that in the future he will change his office to be able to help e.g. horses but for now we can’t allow to register a meeting for cows or camels.
We have aggregate needed knowledge but we forgot about something important. Each appointment is unique, because appointment time cannot be the same for two meetings, also animals can be different. We need to ensure that our aggregate will have an identity and the easiest way to achieve this is to just provide a unique identifier. About creating aggregate identifiers exist a lot of theories, some people create just uuid and place it as a string value but there is an recommendation to have simple Value Object which will handle possible, future changes. Below you can see an example of aggregate id.
export class AppointmentId {
constructor(
private readonly id: string
) {}
static fromString = (id: string): AppointmentId => new AppointmentId(id);
toString = (): string => this.id;
}
Parts of the aggregate
We need to implement data structures which will hold all necessary information in aggregate. I think in our example the best type of aggregate pieces will be value objects. So let’s implement VO for patient and appointment’s time.
export class Patient {
constructor(
private readonly name: string,
private readonly ownerName: string,
private readonly breed: AnimalBreedEnum
) {}
getName = (): string => this.name;
getOwnerName = (): string => this.ownerName;
getBreed = (): AnimalBreedEnum => this.breed;
breedIs = (breed: AnimalBreedEnum): boolean => this.breed === breed;
}
For our patients we have ValueObject with three properties: name, ownerName and breed. I also created a helper method for checking patient breed. According to the definition of VO I made properties private and immutable so we need to add getters to be able to obtain data.
export class AppointmentTime {
constructor(
private readonly length: AppointmentMinsLengthEnum,
private readonly dateTime: Date
) {}
getLength = (): AppointmentMinsLengthEnum => this.length;
getDateTime = (): Date => this.dateTime;
}
Same situation we have for appointment’s time. Two properties (length in minutes and appointment time) and two getters for them.
Appointment Aggregate
Now, having our aggregate parts we can create our appointment representation. We need to cover a few things: creation of a new instance of our aggregate, policies and specification mechanism and of course getters for our VO.
export class Appointment extends AggregateRoot {
constructor(
private id: AppointmentId,
private patient: Patient,
private time: AppointmentTime
) {}
schedule(policies: AppointmentPolicyInterface[], specifications: AppointmentSpecificationInterface[]): void {
this.applyPolicies(policies);
const rejections = this.getSpecificationRejections(specifications);
if (rejections.length > 0) {
this.apply(new AppointmentRejected(this, rejections));
return;
}
this.apply(new AppointmentScheduled(this));
}
patientBreedIs = (breed: AnimalBreedEnum): boolean => this.patient.breedIs(breed);
getId = (): AppointmentId => this.id;
getDateTime = (): Date => this.time.getDateTime();
getAppointmentLength = (): AppointmentMinsLengthEnum => this.time.getLength();
reSchedule = (time: AppointmentTime): void => this.time = time;
private applyPolicies = (policies: AppointmentPolicyInterface[]): void =>
policies.forEach((policy: AppointmentPolicyInterface) => policy.applyFor(this))
private getSpecificationRejections(specifications: AppointmentSpecificationInterface[]): string[] =>
specifications.map((specification: AppointmentSpecificationInterface) =>
specification.isSatisfiedBy(this) ? null : specification.getRejection()
)
.filter((rejection: string | null) => !!rejection);
}
Ouhh, we did it! We have our first aggregate, but what exactly happened? Let’s start with a magic extension from AggregateRoot. To create this example I used the NestJS framework which provides excellent support for CQRS and EventSourcing. In this package we can find an AggregateRoot class which has some useful methods, such as apply() which applies events which can be later handled by the assigned event handler.
Then we have a constructor for obvious purposes. We accept data structures which we defined before. Below the constructor I defined three getters for passed data. We also need one setter for appointment time for one of our policy purposes.
Our core logic is nested in the schedule method. We apply policies via applyPolicy method and we check specifications using getSpecificationRejections. If an appointment has been rejected for some reason then we apply an AppointmentRejected event. In case of no objections from specifications we can fire AppointmentsScheduled event.
Events can be handled by their attached handlers. For example, for success schedule in a handler for the AppointScheduled event we can use a repository and save our aggregate e.g. in the database.
In case if you don’t want to provide an events layer you can easily return something like ResultDTO, or throw an exception in case of rejections. Then, in a higher layer (domain service) use a repository and save the aggregate in case of success. There is no one, unified method to handle this and you should pick the most relevant one for you.
Policies and specifications
I provided a mechanism for applying policies and checking specifications. Let’s create an example of policy and specification. If you don’t know what they are I recommend you check one of my last DDD episodes. For now, policy is a “pattern” to handle temporary business logic changes while specifications are for business rules validation. Both are important parts of aggregates. Let’s consider the following specification: an appointment cannot be scheduled in the past.
export class AppointmentNotInPastSpecification implements AppointmentSpecificationInterface {
private readonly rejection?: string;
isSatisfiedBy(appointment: Appointment): bool {
if (aggregate.getDateTime() < new Date) {
this.rejection = ‘An appointment cannot be scheduled in the past ’;
}
return !!this.rejection;
}
getRejection(): string => this.rejection;
}
Here we can see a simple specification which determines future appointments time. I also created a getter to obtain possible rejection. Now, let’s see an example policy.
export class FreeProphylaxisAgainstTicksForDogDayPolicy implements AppointmentPolicyInterface {
private static readonly worldDogDay = ’26.8’;
applyFor(appointment: Appointment): void {
if (!appointment.patientBreedIs(AnimalBreedEnum.dog)) {
return;
}
const appointentDateTime = appointment.getDateTime();
const apointmentDayMonth = `${appointentDateTime.getDate()}.${appointentDateTime.getMonth()}`;
if (apointmentDayMonth !== FreeProphylaxisAgainstTicksForDogDayPolicy.worldDogDay) {
return;
}
aggregate.reSchedule(new AppointmentTime(
appointment.getDateTime(),
appointment.getAppointmentLength() + AppointmentMinsLengthEnum.quarter
));
}
}
Our vet has a rule for dog patients on a world dog day to process free prophylaxis against ticks. For this purpose he needs 15 more minutes to search the dog’s body for ticks.
Summarize
Okay! That’s it, we provided an example implementation of aggregate. Of course the implementation is not fully completed because I didn’t show you how to persist appointments in the database or even the service layer has not been touched. I know that many things could be done better (e.g. date comparison) but for the example purposes I wanted to minify the amount of code.
Recommended For You:
6 reasons why your next business project needs product design workshops
Design Sprint in product design: 4 things you need to know
Domain Driven Design: discovering domains
Domain Driven Design: domain what?
What’s the difference between software architecture and design?
Domain Driven Design: domain building blocks