Reputation: 3132
New to DDD I have a simple case a I would like to model using DDD approach
2 entities Student
and Course
Relevant property for Student are StudentId and Budget
Relevant property for Course are CourseId and Price
Student and Course are entities that can exists on its own and have their own life cycle
Business requirements:
1) Student can book one course (CourseId is fk for Student table)
2) Student can book the course only if the user's budget is higher or equal to the course price.
3) Changes of course price doesn’t affect the students have already booked the course.
4) When the student book the course the his budget remains unchanged (maybe changes later at the end of the course)
5) Student budget can be modified setting a different amount but new amount have to be higher or equal to the price of the course the user booked.
Setting a lower amount should throw a runtime error.
What the way to model this simple case following domain driven design? Where to enforce the two busines rules (points 2 and 5)?
As a Course can exist without a Student I can’t define the aggregate where Student is the root entity and Course its child entity. Can I?
But at the same time the business rule defined at point 5 seems to me be an invariants. Is it?
So where and how to apply this rules?
I tried a service approach, can work for the first simple rule (point 2) but fail for the rule described at point 5
var student = studentRepository.Get(srtudentId);
var course = courseRepository.Get(courseId)
var studentService = new StudentService();
studentService.SubScribeStudentToCourse(student, course);
studentRepository.Update(student);
StudentService.ChangeStudentBudget(student, 100000);
studentRepository.Update(student);
when I update the student with the new budget someone else can change the course price making the student budget inconsistent
public class StudentService
{
SubScribeStudentToCourse(Studen student, Course course)
{
if (studentt.Budget >= course.Price)
{
student.CourseId = course.CourseId
}
}
ChangeStudentBudget( Student student, decimal budgetAmount)
{
if (student.CourseId != null)
{
var studentCourse = courseRepository.Get(student.CourseId);
if ( studentCourse.Price <= budgetAmount)
{
student.Budget = budgetAmount;
}
else
{
throw new Exception("Budget should be higher than studentCourse.Price");
}
}
}
}
Upvotes: 0
Views: 1382
Reputation: 1210
Your aggregates must be eventually consistent, not strongly, unless it's a really really important scenario. If it is, then consider using Saga, or update them in one transaction. What you should do here is very simple: StudentService.SubscribeTo() and CourceService.Enroll(). This 2 methods should happen in 2 diffent transactions. First, inside StudentService.SubscribeTo you get student and course models from persistence, then you call student.SubscribeTo(course). After the operation, you raise student assignedToCourse Domain Event and StudentDomainEventsHandler catches it, and calls CourceService.Enroll() which gets student and course models from persistence, then calls course.Enroll(student).
Upvotes: 0
Reputation: 4754
Point 5 is another validation rule. But if course price can be modified, you will have to check the rule there too, not just when student budget is modified.
It isn't an invariant. Invariant is regarding just one aggregate. You have two aggregates.
This question sounds to me like I already answered it.
Upvotes: 1
Reputation: 13256
Hypothetical scenarios are typically tricky to comment on but I'll add my ZAR0.02 :)
You will have both a Student
and a Course
aggregate root. If you need the relationship to the other defined then store either a list of ids or a value object that represents the other side.
To enforce certain rules that cannot overlap it may be simpler to have a state regarding the budget on the Student
. For instance, if the course is not in the BudgetApproved
state then you cannot add to course. In order to change the budget you would first need to change the state to, say, 'Budgeting'. In this way you introduce more distinct steps that allow better control of your invariants.
Just another note on changing prices. These things would probably work on a "quote" basis in any event. Once you "Accept" the quote any changes in price are irrelevant unless there is an "Error" or "Omission" that could, and should, be dealt with using some business process or, if not defined in the system, out-of-band. An Order
may be Cancelled
or 'Abandoned` and then some other process such as a reimbursement kicks in.
Upvotes: 2