Problem
In my application a user can lock a set of related aggregate roots to work exclusively on them and to avoid the usage of an invalid set of objects later in the process (by other employees). This problem has nothing to do with the technical concept "optimistic locking", it is a kind of "domain locking".
The model looks simplified something like this:
class Container { val id: ContainerId val isLocked: Boolean = false val lockedBy: UserId? = null } class PartA { val containerId: ContainerId } class PartB { val containerId: ContainerId } All of these objects are aggregate roots and there is no invariant of the Container that could be enforced with information from PartA or PartB. So I think, it was the right choice to model these objects as aggregate roots.
However, neither PartA nor PartB must be edited if the Container is locked. At the moment the lock is enforced at the application service level:
class PartAService( private val containerRepository: ContainerRepository, private val partARepository: PartARepository ) { fun change(data: Something) { val container = containerRepository.findById(data.containerId) if (container.isLocked) { throw ContainerLockedException() } val partA = partARepository.findById(data.partAId) // modify partA ... } } This design is simple and straightforward, but it is also error prone, because you have to remember to perform this check in each related service method.
Possible Solution
My idea to improve the design is to lock each aggregate root instance. To do so a ContainerLocked event would be published that would be handled by handler for PartA and PartB. Both classes would than hold the lock themselves and would check it in methods that modify its state:
class PartA { val containerId: ContainerId val isLocked: Boolean = false val lockedBy: UserId? = null fun modify(x: Int) { if (isLocked) { throw PartALockedException() } // modify state ... } } class PartB { val containerId: ContainerId val isLocked: Boolean = false val lockedBy: UserId? = null fun modify(x: Int) { if (isLocked) { throw PartBLockedException() } // modify state ... } } As you can see there is still the need to check the lock, but it is local now and hopefully much easier to overview.
Technically the locking and unlocking could be implemented with a single update at the database, so that there shouldn't be a big performance impact (one lock/unlock would affect about 3000 rows).
Addendum: This solution would only work for existing objects and is thus only a partial solution. To avoid two approaches, another approach must be found.
Question
What solution would you prefer and why? Is there any better idea?
Containerand there are no relationships betweenContainerinstances. So the number of locked containers has no relevance in this context.