loading...

Immutable ActiveRecord Models

rodreegez profile image Adam Rogers ・3 min read

I've just implemented something that I think is pretty neat in DeskBeers - an immutable ActiveRecord model.

Why?

Why would you want an immutable ActiveRecord model? In our case, the model in question was Subscription. Subscription, in the DeskBeers sense, holds on to the products a customer wants, and the frequency with which a customer wants them. A customer can update the products in their Subscription and the frequency with which they receive them any time, for example this week you might not want your usual order of wine, or you might have a bit of a backlog so might choose to dial deliveries down to being fortnightly for a while. That's totally cool, we got you.

From the information we hold in a customer's Subscription, we then create Order records at the appropriate time. Because a Subscription can change but an Order (once processed, at least) shouldn't, we have to take a copy of the products requested by the Subscription and duplicate that onto the Order. This way, when we look back at past orders, they retain the information of what was sent to the customer and when, regardless of changes in the parent Subscription.

By making Subscription immutable, we get rid of the requirement for the order to know what the customer requested. When the customer makes a change to their Subscription, we create a new Subscription for them, based off the old Subscription, with the required changes. The Order records that pertain to the old Subscription still hang onto the old Subscription, and new ones take their information from the new Subscription.

This allows us to massively simplify Order and make it way more flexible - anything can be added to an Order (anything skuable, that is) and we can add new LineItems to Order way more easily. I'm going off-track a bit here and, not wanting to get into the weeds, let's just say this is a huge win for the flexibility of the DeskBeers platform, honest.

A nice side effect of this change is that, if a customer so wanted to, they can roll back a change made to Subscription as we now maintain a history of the model, which is cool. Or they could treat these different versions as templates, e.g. have one with wine and one without, and alternate between these presets as they saw fit. If they wanted to. Which I'm not sure is a requirement anyone has actually asked for. But it's basically a free feature, perhaps it'll come in useful.

How?

Largely thanks to this post on the Strikingly blog, I created a module like this:

module ActiveRecord
  module Immutable
    class UpdateImmutableException < Exception
      def initialize
        super("Immutable model can't be updated")
      end
    end

    extend ActiveSupport::Concern

    included do
      before_update :prevent_update
    end

    private

    def prevent_update
      raise UpdateImmutableException.new
    end
  end
end        

and included it into Subscription like this:

class Subscription < ApplicationRecord
  include ActiveRecord::Immutable
  # ... code from what is definitely a God model omitted
end

This caused hundreds of test failures. Working through them has proved to be super-interesting, and I've learned a lot about our codebase in fixing them. Making Subscription immutable really highlighted where Subscription and Order were tightly coupled, and fixing the tests has lead me to what I believe will be a way more flexible and ultimately easier to understand codebase.

Do you have any models in your Rails app that could potentially benefit from being immutable?

Posted on by:

rodreegez profile

Adam Rogers

@rodreegez

CEO, developer, bicycle messenger & tea boy @ DeskBeers.

Discussion

markdown guide