Introducing Read Models in your legacy application

Recently on our blog you could read many posts about Event Sourcing. There’re a lot of new concepts around it - event, event handlers, read models… In his recent blogpost Tomek said that you can introduct these concepts into your app gradually. Now I’ll show you how to start using Read Models in your application.

Our legacy application

In our case, the application is very legacy. However, we already started publishing events there because adding a line of code which publishes an event really cost you nothing. Our app is a website for board games’ lovers. On the games’ pages users have “Like it” button. There’s a ranking of games and one of the columns in games’ ranking is “Liked count”. We want to introduce a read model into whole ranking, but we prefer to refactor slowly. Thus, we’ll start with introducing our read model into only this one column - expanding it will be simple. We’ll use just a database’s table to make our read model.

The events which will be interesting for us (and are already being published in application) are AdminAddedGame , UserLikedGame and UserUnlikedGame . I think that all of them are pretty self-explanatory.

But why would you like to use read models in your application anyway? First of all, because it’ll make reasoning about your application easier. Your event handlers are handling writes: they update the read models. After that, reading data from database is simple, because you just need to fetch the data and display them.

The first thing we should do is introducing GameRanking class inherited from ActiveRecord::Base which will represent a read model. It should have at least columns game_id and liked_count .

Now, we are ready to write an event handler, which will update a read model each time when an interesting event occurs.

Creating an event handler

Firstly, we will start from having records for each game, so we want to handle AdminAddedGame event.

class UpdateGameRankingReadModel def handle_event ( event ) case event . event_type when "Events::AdminAddedGame" then handle_admin_added_game ( event ) end end def handle_admin_added_game ( event ) GameRanking . create! ( game_id: event . data [ :game ][ :id ], game_name: event . data [ :game ][ :name ]) end end

In our GamesController or wherever we’re creating our games, we subscribe this event handler to an event:

game_ranking_updater = UpdateGameRankingReadModel . new event_store . subscribe ( game_ranking_updater , [ 'Events::AdminAddedGame' ]

Remember, that this is legacy application. So we have many games and many likes, which doesn’t have corresponding AdminAddedGame event, because it was before we started gathering events in our app. Some of you may think - “Let’s just create the GameRanking records for all of your games!”. And we’ll! But we’ll use events for this : ). However, there’s also another road - publishing all of the events “back in time”. We could fetch all likes already present in the application and for each of them create UserLikedGame event.

Snapshot event

So, as I said, we are going to create a snapshot event. Such event have a lot of data inside, because basically it contains all of the data we need for our read model.

Firstly, I created RankingHadState event.

module Events class RankingHadState < RailsEventStore :: Event end end

Now we should create a class, which we could use for publishing this snapshot event (for example, using rails console). It should fetch all games and its’ likes count and then publish it as one big event.

class CopyCurrentRankingToReadModel def initialize ( event_store = default_event_store ) @event_store = event_store end attr_reader :event_store def default_event_store RailsEventStore :: Client . new end def call game_rankings = [] Game . find_each do | game | game_rankings << { game_id: game . id , liked_count: game . likes . count } end event = Events :: RankingHadState . new ({ data: game_rankings }) event_store . publish_event ( event ) end end

Now we only need to add handling method for this event to our event handler.

class UpdateGameRankingReadModel def handle_event ( event ) ... when "Events::RankingHadState" then handle_ranking_had_state ( event ) ... end ... def handle_ranking_had_state ( event ) GameRanking . delete_all event . data . each do | game | GameRanking . create! ( game ) end end end

After this deployment, we can log into our rails console and type:

copy_object = CopyCurrentRankingToReadModel . new event_store = copy_object . event_store ranking_updater = UpdateGameRankingReadModel . new event_store . subscribe ( ranking_updater , [ 'Events::RankingHadState' ]) copy_object . call

Now we have our GameRanking read model with records for all of the games. And all new ones are appearing in GameRanking, because of handling AdminAddedGame event.

Polishing the details

We can finally move on to ensuring that liked_count field is always up to date. As I previously said, I’m assuming that these events are already being published in production, so let’s finish this!

Obviously, we need handling of like/unlike events in the event handler:

class UpdateGameRankingReadModel def handle_event ( event ) ... when "Events::UserLikedGame" then handle_user_liked_game ( event ) when "Events::UserUnlikedGame" then handle_user_unliked_game ( event ) ... end ... def handle_user_liked_game ( event ) game = GameRanking . where ( game_id: event . data [ :game_id ]). first game . increment! ( :liked_count ) end def handle_user_unliked_game ( event ) game = GameRanking . where ( game_id: event . data [ :game_id ]). first game . decrement! ( :liked_count ) end end

After that you should subscribe this event handler to UserLikedGame and UserUnlikedGame events, in the same way we did it with AdminAddedGame in the beginning of this blogpost.

Keeping data consistent

Now we’re almost done, truly! Notice that it took some time to write & deploy code above it. Thus, between running CopyCurrentRankingToReadModel on production and deploying this code there could be some UserLikedGame events which weren’t handled. And if they weren’t handled, they didn’t update liked_count field in our read model.