I'm trying to decide whether to use a single aggregate or if I can get away with using three separate aggregates.
I have three entities, Quiz, Question, and Answer to represent multiple-choice quizzes. Conceptually, a quiz contains multiple questions, and each question contains multiple answers. Each answer belongs to only one question and each question belongs to only one quiz. A separate quiz is created for each user.
One business rule is that once a quiz is submitted answers can no longer selected or deselected. The quiz's grade is calculated when it is submitted, so if an answer is changed after the quiz is submitted it would put the domain in an inconsistent state.
If I model this as a single aggregate with Quiz as the aggregate root, this is simple enough to enforce. If I model the domain with each of these entities as its own aggregate root then I would have to check at the application level, rather than at the domain level, whether a quiz is submitted or not before selecting or deselecting an answer.
This is a simplified version of what I would do with separate aggregates at the application level:
async execute({ quizId, answerId }: Request): Promise<Response> {
const quiz = await this.quizRepository.get(quizId);
if (quiz.submitted) {
throw new AnswerSelectQuizAlreadySubmitted();
}
const answer = await this.answerRepository.get(answerId);
answer.select();
return await this.answerRepository.save(answer);
}
What I would do with a single aggregate at the application level:
async execute({ quizId, questionId, answerId }: Request): Promise<Response> {
const quiz = await this.quizRepository.get(quizId);
// quiz entity enforces the business rule
quiz.selectAnswer(questionId, answerId);
return await this.quizRepository.save(quiz);
}
I prefer the small-aggregates model because the user can update multiple answers simultaneously without database failures due to optimistic locking at the Quiz level.
My concern with it is that a "submit quiz" command and a "select answer" command could be fired in rapid succession, causing an answer to be selected on an already submitted quiz. My intuition is that the application-level validation I showed above won't necessarily prevent this from happening.
TL;DR: Will an application-level check on a separate aggregate work in an asynchronous environment or will I be forced to include all three entities in a single aggregate?