Introducing Read Models in your legacy application

… and check why 5600+ Rails engineers read also this

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.

But the fix for this is very simple - we just need to run our CopyCurrentRankingToTheReadModel in the production again, in the same way we did it before. Our data will be now consistent and we can just write code which will display data on the frontend - but I believe you can handle this by yourself. Note that in this blog post I didn’t take care about race conditions. They may occur for example between fetching data for HadRankingState event and handling this event.

You might also like