SO style reputation system with CQRS & Event Sourcing
I am diving into my first forays with CQRS and Event Sourcing and I have a few points Id like some guidance on. I would like to implement a SO style reputation system. This seems a perfect fit for this architecture.
Keeping SO as the example. Say a question is upvoted this generates an UpvoteCommand
which increases the questions total score and fires off a QuestionUpvotedEvent
.
It seems like the author's User aggregate should subscribe to the QuestionUpvotedEvent
which could increase the reputation score. But how/when you do this subscription is not clear to me? In Greg 开发者_如何学PythonYoungs example the event/command handling is wired up in the global.asax but this doesn't seem to involve any routing based on aggregate Id.
It seems as though every User aggregate would subscribe to every QuestionUpvotedEvent
which doesn't seem correct, to make such a scheme work the event handler would have to exhibit behavior to identify if that user owned the question that was just upvoted. Greg Young implied this should not be in event handler code, which should merely involve state change.
What am i getting wrong here?
Any guidance much appreciated.
EDIT
I guess what we are talking about here is inter-aggregate communication between the Question & User aggregates. One solution I can see is that the QuestionUpvotedEvent
is subscribed to by a ReputationEventHandler
which could then fetch the corresponding User AR and call a corresponding method on this object e.g. YourQuestionWasUpvoted
. This would in turn generated a user specific UserQuestionUpvoted
event thereby preserving replay ability in the future. Is this heading in the right direction?
EDIT 2
See also the discussion on google groups here.
My understanding is that aggregates themselves should not be be subscribing to events. The domain model only raises events. It's the query side or other infrastructure components (such as an emailing component) that subscribe to events.
Domain Services are designed to work with use-cases/commands that involve more than one aggregate.
What I would do in this situation:
- VoteUpQuestionCommand gets invoked.
The handler for VoteUpQuestionCommand calls:
IQuestionVotingService.VoteUpQuestion(Guid questionId, Guid UserId);
This then fecthes both the question & user aggregates, calling the appropriate methods on both, such as user.IncrementReputation(int amount) and question.VoteUp(). This would raise two events; UsersReputationIncreasedEvent and QuestionUpVotedEvent respectively, which would be handled by the query side.
My rule of thumb: if you do inter-AR communication use a saga. It keeps things within the transactional boundary and makes your links explicit => easier to handle/maintain.
The user aggregate should have a QuestionAuthored
event... in that event is subscribes to the QuestionUpvotedEvent
... similarly it should have a QuestionDeletedEvent
and/or QuestionClosedEvent
in which it does the proper handling like unsibscribing from the QuestionUpvotedEvent
etc.
EDIT - as per comment:
I would implement the Question is an external event source and handle it via a gateway. The gateway in turn is the one responsible for handling any replay correctly so the end result stays exactly the same - except for special events like rejection events...
This is the old question and tagged as answered but I think can add something to it.
After few months of reading, practice and create small framework and application base on CQRS+ES, I think CQRS try to decouple components dependencies and responsibilities. In some resources write for each command you Should change maximum one aggregate on command handler (you can load more than one aggregate on handler but only one of them can change).
So in your case I think the best practice is @Tom answer and you should use saga. If your framework doesn't support saga (Like my small framework) you can create some event handler like UpdateUserReputationByQuestionVotedEvent
. In that, handler create UpdateUserReputation(Guid user id, int amount)
OR UpdateUserReputation(Guid user id, Guid QuestionId, int amount)
OR
UpdateUserReputation(Guid user id, string description, int amount)
. After command sends to handler, the handler load user by user id and update states and properties. In this type of handling you can create a more complex scenario or workflow.
精彩评论