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?
Container
and there are no relationships betweenContainer
instances. So the number of locked containers has no relevance in this context.