Benito Serna Tips and tools for Ruby on Rails developers

Touch + fragment caching to avoid race conditions saving computed values

January 31, 2023

When saving computed values in the database in your rails app, you must be aware that is possible to find unexpected errors in the result due to race conditions.

I have already shared an exercise to help you get more sensitivity about when an implementation can save a wrong value due to race conditions.

Here I want to share one tip you can try to avoid race conditions when saving a computed value.

Using the account balance as an example

To talk about something concrete I will use the “account balance” as an example, but you can use this approach for different types of calculations.

The account balance example

Imagine that you have an Account record that has many entries, and you want to update the balance each time an Entry is created. The balance is the sum of the amount of each entry.

Now imagine that each account will need to create many entries concurrently, maybe on different background jobs or different requests. So if you want to calculate the balance and save it just after an entry is created, you could have problems with race conditions.

Tip: Use touch + fragment caching

If you will use the calculated value mostly in your views, and you need a way of saving the value…

Instead of trying to save the value in the database when an entry is created, you can use touch on the association and try fragment caching.

class Account < ApplicationRecord
  has_many :entries

  def balance
    entries.sum(:amount)
  end
end

class Entry < ApplicationRecord
  belongs_to :account, touch: true
end
<%= cache(account) %>
  <p>Balance: <%= number_to_currency account.balance %></p>
<% end %>

If you need to use the value in a list you can use collection caching, and rails will read all cache templates at once instead of one by one.

<%= render partial: 'account', collection: @account, cached: true %>

Are we really avoiding race conditions?

With this “fix” you are still prune to race conditions, but you are delaying the problem until the value is used.

In my understanding even if you get into race conditions, it would not be that problematic, because even if an other entry is created just after the cache check, when the page is visited again the cache will be refreshed. I can’t see a moment where the cache can get stuck on a previous value.

Is this a perfect fix?

No, you can have other type of problems.

For example, now you have to deal with cache expiration, in the example the calculation just depends on the account.entries. We have an easy way to update the account via touch, but other calculation can have dependencies that are can make the cache expiration more complex.

Do you know other problems with this solution?

If you have experience with other problems with this solution, please leave a comment =)

Related articles

Weekly tips and tools for Ruby on Rails developers

I send an email each week, trying to share knowledge and fixes to common problems and struggles for ruby on rails developers, like How to fetch the latest-N-of-each record or How to test that an specific mail was sent or a Capybara cheatsheet. You can see more examples on Most recent posts or All post by topic.