Caching External APIs in Rails for a Ginormous Speed Boost

The ability to book tours on wework.com is one of our most important sales funnels. We currently use Salesforce as our main manager of tour times and potential leads. Fetching the available tour times from the Salesforce api can take anywhere from 2 to 10 seconds, depending on how hard we’re hitting it. This is clearly not ideal!

First Iteration: Let’s use Rails to cache the response

Using basic Rails fragment caching we cached the response for 24 hours:

  cache_key = ["tour_times", params[:tour_date], params[:building_uuid]]

  result = Rails.cache.fetch(cache_key, expires_in: 24.hours) do
    salesforce_connection.fetch_available_tour_times(params[:tour_date], params[:building_uuid])
  end

We then expired the cache whenever someone booked a tour for that building:

  Rails.cache.delete(["tour_times", opts[:tour_date], opts[:building_uuid]])

This went exceptionally well…at first. Check out this gaudy web external response time graph for this tour times end point:

New Relic Cache Graph

Problems with First Iteration:

WeWork.com isn’t the only place we can book a tour for potential members. Unfortunately, the cache was only being expired when a tour was created on wework.com. We can use Salesforce’s UI among other custom services that we have in our systems to book tours. The cache was becoming stale without us knowing it. BOO!

We also noticed that even when expiring caches based on specific cache keys, there were still inconsistencies with our cache keys (deleting the wrong ones for example). It was a mess keeping track of the correct cache keys and only deleting the necessary ones.

Second Iteration: Use an ActiveRecord object as the cache key

I read this fantastic article by DHH that I’d like to summarize here (although you should still totally read it).

  • Expiring cache values by deleting a cache associated with a static key and re-fetching new data = BAD
  • Using an Active Record object in your cache key, and UPDATING that record to create a brand new cache = GOOD

When you use the Active Record object it just creates a brand new fresher cache key-value pair. There are no consequences because Memcache knows to properly delete the older caches if you run out of memory.

Here’s the new code following the above rules:

  cache_key = [tour_object, params[:tour_date], params[:building_uuid]]

  result = Rails.cache.fetch(cache_key, expires_in: 24.hours) do
      response = sf_connection.fetch_available_tour_times(params[:tour_date], params[:uuid])
  end

The cache_key now allows us to “touch” the most recent tour booked for any location to automatically refresh our cache of available tour times. Now, whenever we book a tour in Salesforce, we have the Salesforce API (through a workflow rule) hit WeWork.com and update the most recent booked tour. It automatically flushes the tour from that building, and we have no more stale tour times.

Also, with the Second Iteration, this graph still looks pretty fantastic:

New Relic Cache Graph 2

Written by Matt Star
Software Engineer at WeWork. React. Redux. Front End.

Related Posts 10

How To Scale As A CTO - Why Course Correction Is Inefficient (Video)
Bridging digital and physical with a MakerBot Replicator 2
How I Aced my Technical Interview (by pretending to be a wizard)
Additional tips on improving scrolling performance of a UICollectionView
Why WeWork.com uses a static generator and why you should too
Rabbits, Bunnies and Threads
Adding columns with default values to really large tables in Postgres + Rails
Creating BEM Stylesheets for React Components
React Tutorial - Converting Reflux to Redux
Inside WeWork's Tech Stack