Ghini Antonio
Ghini Antonio

Reputation: 3132

DDD approach where to enforce business rules without aggregate?

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

Answers (3)

DmitriBodiu
DmitriBodiu

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

choquero70
choquero70

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

Eben Roux
Eben Roux

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

Related Questions