As far as I understand, the big idea behind CQRS is having 2 different data models for handling commands and queries. These are called "write model" and "read model".
Let's consider an example of Twitter application clone. Here are the commands:
- Users can register themselves.
CreateUserCommand(string username)
emitsUserCreatedEvent
- Users can follow other users.
FollowUserCommand(int userAId, int userBId)
emitsUserFollowedEvent
- Users can create posts.
CreatePostCommand(int userId, string text)
emitsPostCreatedEvent
While I use the term "event" above, I don't mean 'event sourcing' events. I just mean signals that trigger read model updates. I don't have an event store and so far want to concentrate on CQRS itself.
And here are the queries:
- A user needs to see the list of its posts.
GetPostsQuery(int userId)
- A user needs to see the list of its followers.
GetFollowersQuery(int userId)
- A user needs to see the list of users it follows.
GetFollowedUsersQuery(int userId)
- A user needs to see the "friend feed" – a log of all their friends' activities ("your friend John has just created a new post").
GetFriedFeedRecordsQuery(int userId)
To handle CreateUserCommand
I need to know if such a user already exists. So, at this point I know that my write model should have a list of all users.
To handle FollowUserCommand
I need to know if userA already follows userB or not. At this point I want my write model to have a list of all user-follows-user connections.
And finally, to handle CreatePostCommand
I don't think I need anything else, because I don't have commands like UpdatePostCommand
. If I had those, I would need to make sure that post exists, so I would need a list of all posts. But because I don't have this requirement, I don't need to track all posts.
Question #1: is it actually correct to use the term "write model" they way I use it? Or does "write model" always stand for "event store" in case of ES? If so, is there any kind of separation between the data I need to handle commands and the data I need to handle queries?
To handle GetPostsQuery
, I would need a list of all posts. This means that my read model should have a list of all posts. I'm going to maintain this model by listening to PostCreatedEvent
.
To handle both GetFollowersQuery
and GetFollowedUsersQuery
, I would need a list of all connections between users. To maintain this model I'm going to listen to UserFollowedEvent
. Here is a Question #2: is it practically OK if I use write model's list of connections here? Or should I better create a separate read model, because in the future I may need it to have more details than write model has?
Finally, to handle GetFriendFeedRecordsQuery
I would need to:
- Listen to
UserFollowedEvent
- Listen to
PostCreatedEvent
- Know which users follow which other users
If user A follows user B and user B starts to follow user C, the following records should appear:
- For user A: "You friend user B has just started following user C"
- For user B: "You've just started following user C"
- For user C: "User B is now following you"
Here's the Question #3: what model should I use to get the list of connections? Should I use write model? Should I use read model – GetFollowersQuery
/GetFollowedUsersQuery
? Or should I make GetFriendFeedRecordsQuery
model itself handle the UserFollowedEvent
and maintain its own list of all connections?
Best Answer
Greg Young (2010)
If you think in terms of Bertrand Meyer's Command Query Separation, you can think of the model as having two distinct interfaces, one that supports commands, and one that supports queries.
Greg Young's insight was that you could separate this into two separate objects
Having separated the objects, you now have the option of separating the data structures that hold the object state in memory, so that you can optimize for each case; or store/ persist the read state separately from the write state.
The term WriteModel is usually understood to be the mutable representation of the model (ie: the object, not the persistence store).
TL;DR: I think your use is fine.
That's "fine"--ish. Conceptually, there's nothing wrong with the read model and write model sharing the same structures.
In practice, because writes to the model are not typically atomic, there is potentially a problem when one thread is trying to modify the state of the model while a second thread is trying to read it.
Using the write model is the wrong answer.
Writing multiple read models, where each is tuned to a particular use case is totally reasonable. We say "the read model", but it is understood that there could be many read models, each of which is optimized for a particular use case, and does not implement the cases where it doesn't make sense.
For example, you might decide to use a key-value store to support some queries, and a graph database for other queries, or a relational database where that query model makes sense. Horses for courses.
In your specific circumstance, where you are still learning the pattern, my suggestion would be to keep your design "simple" -- have one read model that does not share the data structure of the write model.