Introducing StaticCollection: An ActiveRecord Interface for Static Data

At Wealthsimple, we strive to write code that not only runs but is maintainable, reads well, and is a pleasure to work with. In this post, we’ll share one Rails trick that we’ve developed to help us write cleaner, faster, and (we think) better code.

Static data is data that does not change after it is written. Primitive static sets such as a list of strings are often defined in an enum. A non-primitive data structure can be defined as a list of hashes defined at the global level. However, there is no easy way to define larger sets of relatively complex static data.

One approach is to write this data to a YAML file and load it at runtime. YAML allows us to write data in a human-readable format and extracts the messy data details from the class level, which is especially desirable when the static data is on the longer side. The problem with using the loaded YAML data is that it is instantiated as a hash and thus requires hash notation for data access. Perhaps more concerning is that there is no easy way to to query this set; we would have to write a select over the entire set every time we wanted to get a filtered subset. Ideally, we would like an interface similar to ActiveRecord, which not only sets accessor methods but also exposes query methods such as :count, :all, and :find_by_attribute.

Introducing StaticCollections

StaticCollection is a gem that our fearless founding engineer, Peter Graham, created to solve this problem. It is a lightweight layer that loads static data from a YAML file into ruby classes with an ActiveRecord::Base-like interface. It does this by parsing through the YAML to isolate keys and sets these as instance accessor methods on the StaticCollection class. It also defines singleton query methods on the class to filter by attribute.

How we use StaticCollections

We use StaticCollections in production for a multitude of static data sets. One such use case is account types. At Wealthsimple, we offer a multitude of investment accounts for our clients across Canada and the United States. Account types don’t change very often and there are a finite number of them making them a great candidate for a StaticCollection.

- type: "tfsa"
  category: "registered"
  jurisdiction: "CA"
  readable: "TFSA"
  beneficiary_eligible: true
  description: "Tax Free Savings Account. An investment account that grows tax-free. There is a $5,500 annual contribution limit. If you are a Canadian resident and you turned 18 in 2009 or earlier, there is maximum contribution of $52,000 if opening a TFSA for the first time."
  recommended_by_default: true
  ownership_type: "individual"
- type: "joint"
  category: "non_registered"
  jurisdiction: "CA"
  readable: "Joint account"
  beneficiary_eligible: false
  description: "An investment account where two people have joint ownership over the assets. Often makes sense for spouses."
  recommended_by_default: false
  ownership_type: "multi-owner"
- type: "us_individual"
  category: "non_registered"
  jurisdiction: "US"
  readable: "Personal"
  beneficiary_eligible: false
  description: "A basic investment account which is taxed on growth and income. Great if you have maxed out your annual IRA contributions."
  recommended_by_default: true
  ownership_type: "individual"

We define an AccountType class that inherits StaticCollection::Base. We set the data source by loading the YAML file and defining default values. We can further augment this class by defining more specific static methods that operate over the set of static account types.

class AccountType < StaticCollection::Base  
  set_source YAML.load_file('./data/account_types_test.yml'), 
    defaults: {recommended_by_default: false}

  def self.registered_types
    find_all_by_category("registered").map(&:type)
  end

  def self.recommended_by_default(jurisdictions:)
    account_types = all.select do |account_type|
      account_type.recommended_by_default? && jurisdictions.include?(account_type.jurisdiction)
    end
    account_types.map(&:type)
  end
end  

Now we can read AccountTypes the same way we read ActiveRecord models.

> AccountType.find_by_type('joint').ownership_type
=> "multi-owner"

We can also use our new static methods.

> AccountType.registered_types
=> ["tfsa"]

When to use StaticCollections

Before choosing to use a StaticCollection rather than a hard coded list or database, we evaluate whether or not the collection is truly static. Notice that I refer to the collection rather than the data set itself. If the object itself is static but the collection grows significantly over time, this tells us that a traditional database table would be more appropriate. While it is easy to add a few rows to a YAML file, it is still considerably slower to make a change, submit a pull request, merge and deploy than it is to write a new record to a database.

For example, our Core-Services Team recently extracted a StaticCollection of promotions into a database table. The collection was used to identify various promotions that Wealthsimple offers new clients for special events, partnerships, etc. The YAML file was a fitting solution when we had a fixed list of promotion types. Over time, we had to create new promotions and the objects themselves grew in complexity. Moving to a database table enables us to build an API to create new promotions without submitting a pull request.

Some other examples of how we use StaticCollections are countries, provinces, and states. These are all objects that don’t change over time. We also have a collection of financial institutions that we use to map routing codes to bank names, abbreviations, and associated metadata.

How can I apply this to my application?

Today, we’re happy to announce that we’ve open sourced StaticCollection so you can incorporate it directly into your Rails application. Check out the usage guide for some starter tips on how to incorporate the gem.

And if you like writing clean code and augmenting the frameworks you work within, check out our careers page and drop us a line!