2

Lets say we have DailySchedule Aggregate Root and Appointment Entity. DailySchedule AR owns a collection of Appointments. The idea is - we want to enforce the following business rule given by domain experts:

Daily Schedule cannot have two or more Appointments with the same time slot (e.g. Appointment starting 09:00 AM and ending 10:00 AM).

Now imagine two users at the same time are modifying Daily Schedule AR - adding a new Appointment with starting time of 09:00 AM and ending time of 10:00 AM. Both inserts will succeed because when AR was loaded initially and new Appointment was added, business rule was not violated, however in the end AR will be in invalid state.

How can this be avoided? Two of my ideas:

  1. Pessimistic db locking - lock users from modifying AR while another person is modifying it.
  2. Allow AR to be in invalid state and eventually somehow fix it, some sort of a process or event (eventual consistency).

1 Answer 1

2

Both inserts will succeed because when AR was loaded initially and new Appointment was added, business rule was not violated, however in the end AR will be in invalid state.

That's the lucky version. In the unlucky version, you get a lost edit: when the second change is made, the first is overwritten and lost.

You've actually got two orthogonal concerns going here. The first is the lost update problem. We need to ensure that the persistence mechanism doesn't destroy information when we have racing edits.

The usual mechanism here is locking; we ensure that only one process can hold the lock at any given time, and ensure that the process holding the lock maintains the consistency of the data.

Two common patterns

lock.acquire
db.read
(business logic)
db.write
lock.release

Because the read occurs after the acquisition of the lock, we are protected against the situation that some other process sneaks in and changes the data we just read.

db.read
(business logic)
lock.acquire
db.compare
db.write
lock.release

Same basic idea: we ensure that only one process can write to the database at any given time. The difference here is that we explicitly compare the state of the database after we acquired the lock to the state we read originally. In other words, we're performing a compare and swap.

It's fairly common to use a mechanical comparison; for instance, for every write to increment a version number; compare then need only check that the version number after acquiring the lock matches the one used to read the data. This does, of course, assume that the increment of the version happens as part of the same atomic transaction as the changes to the business data.

Completely separate is the modeling question: should a conflicting appointment be rejected, or should it instead be accepted and handled as an escalation.

It would be useful to review Race Conditions Don't Exist.

A microsecond difference in timing shouldn’t make a difference to core business behaviors.

So for something like an appointment calendar, where the appointments describe the state of the Real World[tm] and you have multiple collaborators contributing information to the system, you probably want to make conflict an explicit part of the model (for example, maybe the second appointment goes into a "wait list" if the time slot is already booked).

But notice that switching to a model where you accept the double booking and escalate does not itself fix the lost edit error. You still need the locks to ensure that the first appointment is visible to the second.

1
  • Thanks a lot! Really appreciate it. Also I didn't think of it first, but if I was really paranoid I could make SQL indexes on [datetime][start][end] columns. Jun 30, 2020 at 20:34

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.