I am designing an aggregate that has list of keywords as its property, and it has constraint that two aggregate cannot have same keywords as its property.
I use repository
that has save
and saveAll
method when persisting aggregate, and each run in a single transaction. And Factory
pattern is used when creating an aggregate.
When creating a single aggregate, the validation would be easy that i check if keyword is owned by others through KeywordChecker
service that internally query the existing record for the validation.
The problem comes when i create multiple aggregates, in this case not only checking existing record with KeywordChecker
is required, but also checking pre-created, but not yet persisted aggregates is required, because KeywordChecker
will only check the records that are persisted.
I could put another validation logic that resembles KeywordChecker
but that will lead me to duplicate service just working on a different dataset. Is there any elegant way to handle this issue?A
1 Answer 1
As you have stated, once the data is persisted it's not really a problem - any subsequent action can simply check if a conflicting record/aggregate exists.
Fundamentally the problem you are facing is one of distributed locks - you need to take a lock early on in the processing, before you are ready to persist the primary data/transaction, so that other operations can have visibility into the existence of the lock.
In order to achieve that you need to ensure that the lock is not part of the primary transaction - that might be a second DB connection (if you want to use the database for managing the lock) or you could use a dedicated service to manage the locks (such as Redis) I am assuming that the primary transaction will commit in some reasonably short amount of time so you can use a simply strategy such as locks expiring after 10 seconds to ensure that you don't end up with orphaned locks, if there is some bug in your code.
Edit: Integration of Locks into the design
From a business perspective I would consider the Keywords
to be a finite resource - i.e. there is only one of each that can be REFERENCED in the main aggregate. I see this as analogous to Orders
and Inventory
i.e. if the business requirement is that items can only be added to Orders
if they are in stock, it becomes necessary to update both aggregates in sync (i.e. using domain events to propagate updates across aggregates).
However the point of Aggregates is that they are transaction boundaries, so in the case of the inventory example, you would probably reserve the inventory first, assign it to the order then decrement the inventory (and reservation) after the order has been shipped (i.e. the reservation remains in place until you no longer have the physical item in the warehouse).
Now applying this to keywords
I think the root of the problem is the keyword
is actually it's own aggregate, albeit a tiny one with almost no functionality. If you were to implement this purely as a DDD solution I think you would use a table just to store the keywords that were in use (or about to be used).
Your KeywordChecker
service is conceptually the repository for Keyword
aggregate - I understand it doesn't really make sense to create a table for this limited functionality (and implement all the logic to update it correctly), however I think you can still expose it as such. I.e. that the implementation of the persistence of keywords
is based on a lock and checking the existence of those keywords in the main aggregate rather than using its own storage.
-
Thank you for the answer, How should i adapt lock into my domain logic? The first thing comes into my mind is creating a
KeywordLock
domain service that haslock
, andunlock
method receiving keyword as the parameter. And theKeywordChecker
service will check both db andKeywordLock
service(by checking if it can acquire a lock or not) within itscheck
method. And my factory and aggregate will use theKeywordLock
service. Nonetheless, it seems like that i brought infra concerns into my domain logic, since my domain has nothing to do with the termlock
.ringord– ringord2023年12月20日 06:14:02 +00:00Commented Dec 20, 2023 at 6:14 -
Edited answer to answer this locking question.DavidT– DavidT2023年12月20日 08:19:50 +00:00Commented Dec 20, 2023 at 8:19
-
Thanks, instead of
KeywordChecker
service, i could useKeywordPool
that haspull
andpush
method, thatpull
will check the availability of keyword, acquire the lock, andpush
will just release the lock. And my factory will depend on theKeywordPool
. And while writing this, a new concern comes into my mind. Although the aggregate created by factory is created, it isn't actually created in the whole system until it is persisted. So if the created aggregate is not persisted due to connection/storage error, the acquired lock must be released. continued...ringord– ringord2023年12月20日 09:16:15 +00:00Commented Dec 20, 2023 at 9:16 -
To do so, not only the factory depends on the
KeywordPool
but also the domain orchestration method(I call this task handler) needs to depend onKeywordPool
to release the lock (if i use lock without ttl feature). It feels like having another transaction boundary surrounding our task handler. Would that be ok?ringord– ringord2023年12月20日 09:23:45 +00:00Commented Dec 20, 2023 at 9:23 -
@DavidT before talking about locks, reservations and advanced concurrency management of the kind used by online stores, shouldn't we look at the domain context? What is the contention for these keywords and the impact of conflicts? If it is low and conflicts are rare, optimistic concurrency control - basically boiling everything down to "once the data is persisted" - seems like a sensible option.guillaume31– guillaume312023年12月20日 14:21:58 +00:00Commented Dec 20, 2023 at 14:21
KeywordChecker
will still check that the keyword is owned by the first aggregate, and user will not allow to create second one. b) That should not be allowed. If i use unique constraint of database, it will be easy to do so, but then our domain model will miss the important business concern(that the same keyword cannot be owned by multiple aggregate) .