Extract Mostly-Constant Data from the Database

Tute Costa

Using database-backed mostly-static models in our applications can cause coupling and performance problems. Extracting that data to Ruby constants helps to resolve those problems.

Example

Consider the following database tables and their data:

  • subscription_plans (subscriber free, subscriber paid, group free, group paid)
  • countries (United States, India, Belgium, Uruguay, etc.)

These change so rarely that for the purpose of the application, it is essentially constant. Representing the data in our database has the following drawbacks:

  • Code/database coupling
  • Slower performance

Code/database coupling

If we put constant data in the database, our code and the database will need to exist in a very specific shape at a given time.

Every environment (development, staging, production) will need to know the values and seed them into their respective databases.

When we need to deploy code changes, we may need to include a data migration to change the data. Particularly in environments that gradually roll out features, this may complicate our development, git branching, and deploy processes by forcing us to consider questions such as:

  • Do we run the migration before or after the code changes are deployed?
  • During the roll out to a percentage of users and app servers, will both old and new code work on the migrated database or will one cohort get bugs?

Slower performance

While 99% of our traffic patterns should be served from our database cache, we will still be paying a small performance penalty for accessing the database across a network.

In comparison, if we move our data to constants in our application, we will already have it in memory when the application loads. There is essentially no performance hit to pull it from memory when we need it.

Solution: Constants and Plain Old Ruby Objects

Here is an example class that can replace the subscription_plans table:

class SubscriptionPlan
  def initialize(slug, name, braintree_id = nil)
    @slug = slug.to_s
    @name = name
    @braintree_id = braintree_id
    raise 'plan slug not recognized' unless SLUGS.include?(@slug)
  end

  SLUGS = %w(subscriber_free subscriber_paid group_free group_paid).map(&:freeze).freeze
  SUBSCRIBER_FREE = new('subscriber_free', 'Free Subscription')
  SUBSCRIBER_PAID = new('subscriber_paid', 'Monthly Subscription', 'user_monthly')
  GROUP_FREE = new('group_free', 'Group Subscription')
  GROUP_PAID = new('group_paid', 'Group Subscription', 'group_monthly')

  attr_reader :name, :slug

  def braintree_plan?
    braintree_id.present?
  end

  def price
    price_in_cents.to_f / 100
  end

  def price_in_cents
    braintree_plan? ? 5000 : 0
  end
end

The application no longer needs to have up-to-date seeds, neither its test suite to have factories in sync to be able to run.

We no longer need database migrations if a new subscription plan is added in the future. Instead, the constantized data change alongside with the code that consumes it, making deploys simpler.

What’s next

If you found this useful, you might also enjoy Don’t Be Normal.