Think Before You Cache

Ten Bitcomb
KPCC Labs
Published in
7 min readDec 6, 2017
There are only two hard things in Computer Science: cache invalidation and naming things. — Phil Karlton

If the phrase “it’s a caching issue” is a regular one on your team, that’s a sign of insidious flaws in your application design.

Caching has its place, and it’s nearly impossible to run an application of any complexity without storing precomputed data in some way. But when it doesn’t work (i.e. there’s a bug in your cache invalidation), it can cause astonishing problems that may be difficult to diagnose. It can waste developer time, which is expensive, and excessive caching is a sign of poor planning.

Rails, a popular web framework among startups, allows developers to cache specific parts of web pages by simply declaring them as cached. For example, if a sidebar requires a lot of computation to show content and messages the user has interacted with, a developer can simply slap a “fragment” cache around it. Rails is not the only framework that supports fragment caching, nor the first one to come up with it, but has played a big role in popularizing it. It’s a very powerful feature that’s helped countless developers build production-ready web applications.

It’s easy. Too easy.

The problem with techniques like fragment caching is that they’re too easy, and too flexible. You can have caches within caches within caches, which can be beneficial but can also make your code harder to reason about.

You can tell just how common a problem is from the abundance of meme images for it.

Many junior developers learn Rails and are taught about fragment caching, which is a feature that comes built-in, but seldom if ever are we taught the potential pitfalls of caching in general. It seems like pure magic to just wrap a cache block around code and suddenly your page instantly loads. Why wouldn’t you cache all the things?

Caching is often fraught with complication, especially if different caching strategies are working on top of each other and the cache of one data model depends on the state of another related data model. At some point, every web developer will come across curious circumstances where users aren’t seeing pages update with the data they submitted — the prime suspect will always be a problem with expiring the cache.

Caching is like dealing with gravity; at very small scales, the effect of gravity is so slight that in some fields, like chemistry, it’s not even taken into account. At larger scales, you must take it into consideration and try your best not to fall too hard when your calculations are off. You can choose to use an airplane or a pogo stick to fight gravity, the latter requiring less money and skill at a greater risk of injury. Better yet, you can also decide that you don’t even need to leave the ground at all.

Always rethink things.

Even if you could pull off lots of caching without side effects, its use should always cause you to reevaluate your application’s fundamental architecture and design patterns. Caching adds complexity, however small. Good development should remove as much complexity as possible, but the negative with caching is it encourages taking existing problems and sweeping them under the rug as if they no longer exist.

Caching should be the last option, not the first.

Here is a list of questions to ask before you go down the “just cache it” route:

1. Can your data eventually be consistent?

Let’s say you’re building a custom CMS, and lots of relational queries need to be performed in order to display related posts and content; if ~10 seconds of delay before your readers see updates to content is not a problem, caching your data in the background might be less complicated and more manageable than caching fragments of your template. Better yet, you can use a database such as PostgresQL or CouchDB to build “views” that perform these operations outside your application code.

Isn’t that just caching? Yes, but this keeps logic around caching out of the request/response cycle (as well as your templates), and possibly outside of your main codebase all together. The main difference is you are caching the data itself rather than fragments of HTML. Whenever possible, I would opt to cache the data so that the state of pages and components stays manageable and malleable.

2. Does your data need to be relational?

Imagine your custom CMS needs to support bi-directional related links for each of your blog posts. This means you’ll need to make a somewhat expensive query to combine the links in your blog post with links to other blog posts that reference the current one. Just cache the “related links” section on the blog post page, you say? That would technically work.

Wait… why are we running queries for things that are only ever shown in the context of a particular blog post?

If instead you used a NoSQL database like MongoDB, you could simply “embed” your related links in your blog post records. That way, one query to grab a blog post also brings in the related links for that post. A separate operation on-save could add links to other blog posts that reference the current one. Barring any other complexities, a page could make just one query for the blog post with no caching required.

Credit: Sigfrido ”Sig” Narváez, Sr. Solutions Architect, MongoDB

With this setup, it’s possible that related links might “expire” if the post at a link gets deleted. Link titles may get out of sync, too. This might be a problem for you, or it might not.

Is that in and of itself caching? Superficially it would seem that way, but if blog post records are the one source of truth for related links, then all you’ve done is store your data in an optimized fashion.

3. Do users value your data?

There are a lot of preconceived notions out there in web development, and a lot of “must haves.” But user behavior can vary depending on the kind of site you run and what audience it fosters. Perhaps your users aren’t interested in related links. If that’s the case, then why are you cluttering up their experience and wasting server resources?

As a developer, it’s difficult to value implementing analytics, which for me is usually an annoyance because I just want to write code. I’m sure I’m not the only one who feels this way. Yet I’ve learned to appreciate analytics and user-testing in order to figure out what people actually value, which can have a positive impact on both application performance and complexity.

Obvious? Perhaps, but the obvious often goes unappreciated.

4. Are there trade-offs you can make instead?

Quibbling over shaving 300ms from your response time? Maybe the answer lies not so much in cacheing, but whether or not you should be using a behemoth like Rails to begin with. To be enable rapid development of web applications, Rails comes with a whole stack of features out of the box to make magic happen. This does come at a cost, though decreasingly as Rails continues to mature. Still, part of the philosophy of the Ruby community is that if you want something, there’s a gem for it, so save yourself the time and just include it in your Gemfile.

The proliferation of Ruby gems is both a strength and a weakness in the community. It’s allowed for rapid development, but each of these things come with a cost; every time you decide not to write something itself, you’re loading someone else’s library into memory along with extra code to support a lot of features you may not even need. Adding dozens, perhaps hundreds of gems to your Gemfile may be doing more harm than good.

I’m not saying I have to cache it, but I have to cache it.

If you do have to use caching, my first suggestion would still be to cache the data rather than fragments of your markup. Generating HTML from templates should not be a very expensive operation; if it becomes one, and your database queries are blazing fast, either your rendering engine has a fundamental problem or you need to scale your servers.

If you are using Rails, instead of rendering parts of your markup with ActionView Partials, you can implement a more component-based pattern with Cells.

The beauty of Cells is that they are more object-oriented than partials and fragment caches, they make it easier to keep logic outside of their templates, and because they are very portable that means that you can actually test your cacheing implementation. You can of course do the latter with ActionView, but it’s much more difficult than just instantiating your Cell class like any other Ruby object.

Conclusion

Caching is making a deal with the devil. In deciding to use it, you should ask yourself many questions before going down that path. While caching can definitely benefit application performance, it shouldn’t be the de facto tool for making your application faster, because using it too haphazardly can lead to some peculiar bugs. Certain caching implementations clutter up code, and it’s important that developers wrap their heads around as few complications as possible so they can keep as much brain power free for other creative efforts.

When dealing with a performance issue, the first idea you come up with should not be caching — that should be considered last.

--

--