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.
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
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?
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
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.
If you found this useful, you might also enjoy don’t be normal.
This article was based on the original notes in my blog.