I'm working on a Training Enrollment web application with the following (simplified) features:
- Maintenance (Add, Edit, Deactivation) of Trainings
- Student sign-up
- Student enrollment to Active Trainings
I've employed Onion Architecture. All my business logic/validation is in the Domain Layer and the Data Access Layer consists of ADO.NET components and stored procedures (no business logic whatsoever). The problem is that since the business logic/validation is in the domain, there may be a race condition in the following scenarios:
Training A allows a maximum of 50 students only
Training A currently has 49 students enrolled
In the web application, two users are attempting to enroll simultaneously. At this point two threads spawned from two concurrent requests to the web application are able to query 49 users, so both validations succeed in the Domain layer and both users are able to enroll.
Training A is created
Student A is attempting to enroll to Training A
While Student A is attempting to enroll to Training A, a Training Admin decides to deactivate Training A. A race condition occurs when in the Domain layer, when the following happens: In thread 1 of User 1, validating whether Training A is active succeeds (returns true) and at the same time;
- In thread 2, the thread on which Training A is being deactived validates that there are no enrollments yet; so that both validations succeed.
An obvious solution is to add business logic (checking for enrollment count, checking whether a Training is active) in my stored procedures but I'm hesitant to do this as it breaks the design.
My validation logic has a single point of entry and I can add a lock to such that the validation logic becomes a critical section so only one operation (validation and DB persistence) happens at a time, but this reduces concurrency.
Any alternative approach or how has this solved in the systems you've worked on?