In my last article, I presented some code that wrapped up accessing a customer’s Stripe data and added a caching layer on top. I wanted to take some time to dig in to that code and see how we can make it better.

Decorators give us a tool to add additional functionality to a class while still keeping the public API consistent. From the perspective of the client, this is a win-win! Not only do they get the added behavior, but they don’t need to call different methods to do so.

The Problem

Our original class accessed data from Stripe AND cached the response for some time period. I accentuated “AND” because it’s generally the word to be on alert for when considering whether functionality can be teased apart in to separate responsibilities.

The question becomes, can we make one class that accesses Stripe data, and another that’s only responsible for caching it?

Of course we can!

The Solution

Let’s start with the most basic form of accessing our Stripe customer data with the Stripe gem:

class AccountsController < ApplicationController before_action :require_authentication def show @customer = Stripe::Customer.retrieve(current_user.stripe_id) @invoices = @customer.invoices @upcoming_invoice = @customer.upcoming_invoice end end

Extract an Adapter

Because we’re interfacing with a third-party system (Stripe), it makes sense for to create a local adapter to access the Stripe methods. It’s probably not likely we’re going to switch out the official Stripe gem for another one that access the same data, but a better argument might be that we could switch billing systems entirely in the future. And if we make a more generic adapter to our third-party billing system, we would only need to update our adapter when that time comes.

While the adapter optimization may seem like overkill here, we’ll see how that generic adapter helps us implement our caching layer shortly.

Let’s start by removing the notion that it’s Stripe and all and call it Billing . Here we can expose the methods needed from the AccountsController above:

class Billing attr_reader :billing_id def initialize(billing_id) @billing_id = billing_id end def customer Stripe::Customer.retrieve(billing_id) end def invoices customer.invoices end def upcoming_invoice customer.upcoming_invoice end end

There we have it. A simple Billing class that wraps the methods that we used in the first place — no change in functionality. But certainly more organized and isolated.

Let’s now use this new class in the accounts controller from earlier:

class AccountsController < ApplicationController before_action :require_authentication def show billing = Billing.new(current_user.stripe_id) @customer = billing.customer @invoices = billing.invoices @upcoming_invoice = billing.upcoming_invoice end end

Not too bad! At this point we’ve provide the exact same functionality we had before, but we have a class that sits in the middle between the controller and Stripe gem – an adapter if you will.

Create a Decorator

Now that we have our adapter set up, let’s look at how we can add caching behavior to improve the performance of our accounts page.

The most of basic form of a decorator is to pass in the object we’re decorating ( Billing ), and define the same methods of the billing, but add the additional functionality on top of them.

Let’s create a base form of BillingWithCache that does nothing more than call the host methods:

class BillingWithCache def initialize(billing_service) @billing_service = billing_service end def customer billing_service.customer end def invoices customer.invoices end def upcoming_invoice customer.upcoming_invoice end private attr_reader :billing_service end

So while we haven’t added any additional functionality, we have created the ability for this class to be used in place of our existing Billing class because it responds to the same API ( #customer , #invoices , #upcoming_invoice ).

Integrating this new class with AccountsController looks like:

class AccountsController < ApplicationController before_action :require_authentication def show billing = BillingWithCache.new(Billing.new(current_user.stripe_id)) @customer = billing.customer @invoices = billing.invoices @upcoming_invoice = billing.upcoming_invoice end end

As you can see, we only had to change one line — the line where we decorated the original billing class:

BillingWithCache.new(Billing.new(current_user.stripe_id))

I know what you’re thinking, “But it doesn’t actually cache anything!”. You’re right! Let’s dig in to the BillingWithCache class and add that.

Adding Caching Functionality

In order to cache data using Rails.cache , we’re going to need a cache key of some kind. Fortunately, the original Billing class provides a reader for billing_id that will allow us to make this unique to that user.

def cache_key(item) "user/#{billing_id}/billing/#{item}" end

In this case, item can refer to things like "customer" , "invoices" , or "upcoming_invoice" . This gives us a method we can use internally with BillingWithCache to provide a cache key unique to the both the user and the type of data we’re caching.

Adding in the calls to actually cache the data:

class BillingWithCache def initialize(billing_service) @billing_service = billing_service end def customer key = cache_key("customer") Rails.cache.fetch(key, expires: 15.minutes) do billing_service.customer end end def invoices key = cache_key("invoices") Rails.cache.fetch(key, expires: 15.minutes) do customer.invoices end end def upcoming_invoice key = cache_key("upcoming_invoice") Rails.cache.fetch(key, expires: 15.minutes) do customer.upcoming_invoice end end private attr_reader :billing_service def cache_key(item) "user/#{billing_service.billing_id}/billing/#{item}" end end

The code above caches the call to each of these methods for 15 minutes. We could go further and move that to an argument with a default value, but I’ll leave as an exercise for another time.

Summary

Separating your application and third-party services helps keeps your applications flexible — offering the freedom to switch to another service when one no longer fits the bill.

Another benefit of an adapter is you have the freedom to name the class and methods whatever you like. The base gem for a service might not have the best names, or it may be that the names don’t make sense when dragged in to your application’s domain. This is a small but important point as applications get larger and its code more complex. The more variable/method names you need to think about when you poke around the code, the harder it’ll be to remember what was going on. Not to mention the pain new developers will have if they acquire the code. Whether it’s you or the next developer, the time you invest in creating great names will be greatly appreciated.