Reputation: 1997
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?
Upvotes: 2
Views: 277
Reputation: 21769
This single statement
Each answer belongs to only one question and each question belongs to only one quiz.
renders the entire rest of the question moot. The answer is that you'd use a single aggregate in your domain model.
How you deal with database contention is a separate, albeit important, concern that could perhaps be remedied by using a document database where the document is the aggregate root and its children.
My answer would be different if your domain was richer.
For example, you could have a Question aggregate with a list of Answer and a ValidAnswer. If you randomly generated a Quiz and selected from a list of Question aggregates, then you'd have a Quiz aggregate with a list of QuizQuestion (or QuesionReference or some such) as children. Thus your Quiz aggregate references (not owned/separate aggregate) Questions through an (owned/value object) QuestionReference. In this scenario, you're safe from some contention because you're still updating the Quiz aggregate, and not the Question aggregates.
Dealing with click-happy 😀 users
Your domain model in either case must check that answer selection can't occur after quiz submission. That's logic on your Quiz aggregate.
Now, that said your concern that a race condition could occur is valid. The simple model is easy; a single aggregate to a single transaction or single document naturally prevents the race condition; just use normal db concurrency options. But, the two-aggregate model does too! Why? You're still updating only the Quiz aggregate. The Question aggregates are referenced, and don't change (well at least in quiz-answering context).
Finally, in all of the above, Answer does not seem like a separate aggregate or entity. How can an Answer mean anything without being tied to a Question? Maybe in Jeopardy!?
Upvotes: 1