CQRS – Understanding Write and Read Models Without DDD

cqrsread-model

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) emits UserCreatedEvent
  • Users can follow other users. FollowUserCommand(int userAId, int userBId) emits UserFollowedEvent
  • Users can create posts. CreatePostCommand(int userId, string text) emits PostCreatedEvent

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)

CQRS is simply the creation of two objects where there was previously only one.

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.

interface IChangeTheModel {
    void createUser(string username)
    void followUser(int userAId, int userBId)
    void createPost(int userId, string text)
}

interface IDontChangeTheModel {
    Iterable<Post> getPosts(int userId)
    Iterable<Follower> getFollowers(int userId)
    Iterable<FollowedUser> getFollowedUsers(int userId)
}

class TheModel implements IChangeTheModel, IDontChangeTheModel {
    // ...
}

Greg Young's insight was that you could separate this into two separate objects

class WriteModel implements IChangeTheModel { ... }
class ReadModel  implements IDontChangeTheModel {...}

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.

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?

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.

Here is a Question #2: is it practically OK if I use write model's list of connections here?

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.

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?

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.