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.