Make your Rails App Configurable in 4 Ways
Pimp My Ruby
Posted on August 1, 2024
Over the years, I have participated in several Ruby on Rails projects that implement the same functionalities but in completely different ways.
Today, I would like to share with you the 4 ways I have found most effective for making the projects I have worked on Configurable.
Introduction
Configuring an application means giving administrators the ability to provide the context your application needs to operate.
Let's say your application has a paid subscription. As a developer, you want to "give control" to administrators to update prices. You want to be able to call a "variable" directly in the code, which is accessible everywhere. This is how you will make your application configurable.
Table of Contents
1. Constants
2. Configuration File
3. Configuration Model
4. Variable Model
TL;DR
Conclusion
1. Constants
Explanation
The simplest and most naive way to make your application configurable is by assigning constants to your classes.
There are generally two types of constants:
Global constants, which you will use in multiple places in your application:
# config/initializers/global.rb
class Global
SOFTWARES = %w[libreoffice word textedit].freeze
BRAND_NAME = "YourNameHere"
end
# You can use it everywhere in your app like this
Global::BRAND_NAME = "YourNameHere"
Or constants that are specialized for a feature or class:
class UpdateUserFirstNameService
FORBIDDEN_FIRST_NAME = "Julien"
def call(user:, first_name:)
raise 'Forbidden first name' if first_name == FORBIDDEN_FIRST_NAME
user.update(first_name: first_name)
end
end
These constants are directly accessible throughout the application without going through a database or external files.
Pros
- Simplicity and Speed: Immediate access to configuration.
- Security: Fewer risks of accidental modifications since constants are defined in code and not modifiable via an admin interface.
Cons
- Lack of Flexibility: Any changes require code modification and redeployment of the application, which can be cumbersome for configurations that may need to change more regularly.
- Not Suitable for Dynamic Configurations: Not practical for data that needs to be updated regularly or is too voluminous to be efficiently managed as constants.
- Lack of Administrative Control: Non-technical administrators cannot update configurations without going through developers.
When to Use It?
In my opinion, this approach should only be used when you want to configure static elements. Examples include API keys, values useful throughout your application, or data for static pages.
2. Configuration File
A slightly more advanced way to configure your application is via configuration files.
Explanation
In a .yml
or .csv
file, you can store all the data you need in a hierarchical format. You can then exploit this data by reading it when the application starts. We then need to expose these values to our application through a class that contains a constant.
Here's an example:
# config/app/airports.yml
AYHK:
name: Hoskins Airport
country: US
[...]
EFVR:
name: Varkaus Airport
country: FI
[...]
[...]
# app/services/find_airport_service.rb
class FindAirportService
AIRPORTS = YAML.safe_load(Rails.root.join("config/app/airports.yml").read)
.with_indifferent_access.freeze
class << self
def from_id(id)
AIRPORTS.fetch(id)
end
def from_country(country)
[...]
end
end
end
Pros
- Developer Control: Modifications are controlled by developers, reducing the risk of accidental errors.
- Performance: Quick reading of configurations at startup without database queries.
Cons
- Less Flexible for Administrators: All changes require developer intervention and application restart.
- Management Difficulties for Large Data Sets: Complications can arise when information is numerous or when data structures evolve.
When to Use It?
We already use .yml
files in our Rails projects for I18n
. Configuration files are very useful for storing data that we don't want in the database but is too numerous for simple constants.
3. Configuration Model
Now that we've seen static configurations, let's look at dynamic configurations by introducing the database.
Explanation
We'll set up a Configuration
model. Each attribute of this model corresponds to a specific variable. We could represent the table as follows:
# inside db/schema.rb
create_table "configurations", force: :cascade do |t|
t.float "variable_1", default: 5.0, null: false
t.integer "variable_2", default: 160, null: false
t.bigint "admin_id", null: false
t.boolean "active", default: true, null: false
t.index ["admin_id"], name: "index_configurations_on_admin_id"
end
- admin_id: Identifies who set up the configuration.
- active: Identifies the configuration to use.
- variable_1 and variable_2: Variables stored in the database.
The idea is to have a Configuration
model that exposes quick and lightweight access for the application.
Here's how I like to implement it:
# app/models/configuration.rb
class Configuration < ApplicationRecord
LOOKABLE_ATTRIBUTES = %i[
variable_1
variable_2
].freeze
class << self
def active
Rails.cache.fetch('active_configuration') do
Configuration.find_by!(active: true).to_h
end
end
def to_h
LOOKABLE_ATTRIBUTES.index_with do |attribute|
send(attribute)
end
end
LOOKABLE_ATTRIBUTES.each do |attribute|
define_method(attribute) do
active[attribute]
end
end
end
end
# rails console
[1]> Configuration.variable_1
Configuration Load (1.9ms) SELECT "configurations".* FROM "configurations" WHERE "configurations"."active" = $1 LIMIT $2 [["active", true], ["LIMIT", 1]]
=> 5.0
[2]> Configuration.variable_2
=> 160
Next, I just need to expose my Configuration model through the admin interface. When an admin wants to update the Configuration, a new record is created. This record has active: true
, and it invalidates the cache in the process.
If at any point there's a configuration issue, I can always roll back simply by modifying the active
attribute.
Pros
- Control and Flexibility: Administrators are autonomous while developers retain code control.
- Security and Traceability: Each change can be traced to a specific admin, and configurations can be easily activated or deactivated.
- Good Performance: Uses caching to avoid repetitive database queries, thus improving performance.
Cons
- Cache Management: Requires an effective strategy for cache invalidation when configurations are updated.
- Adding Variables is Cumbersome: To add a variable in your Configuration model, you need to: Make a migration, update your model, update your admin interface, and deploy.
When to Use It?
Perfect for environments where configurations need to be frequently updated by administrators, but adding variables is rare.
4. Variable Model
The Configuration
model has a limitation: adding variables is restricted by the developer.
Let's see how to break this glass ceiling with the Variable
model.
Explanation
This method uses a Variable
model where each record represents a different variable. Let's see its structure right away, and I'll explain afterward:
# db/schema.rb
create_table "variables", force: :cascade do |t|
t.string "name", null: false
t.string "value", null: false
t.string "type", null: false
t.boolean "array", default: false, null: false
end
The attribute names are quite clear. Let's look at an example of implementation I use for this approach:
# app/models/variable.rb
class Variable < ApplicationRecord
enum type: { string: 'string', integer: 'integer', float: 'float', boolean: 'boolean' }
class StringResolver
def self.resolve(value)
value.strip
end
end
class IntegerResolver
[...]
end
class FloatResolver
[...]
end
class BooleanResolver
[...]
end
class << self
def fetch(name)
variable = find_by!(name: name)
resolver = "#{variable.type.camelize}Resolver".constantize
if variable.array
variable.value.split(',').map { |v| resolver.resolve(v) }
else
resolver.resolve(variable.value)
end
end
end
end
# rails console
[1]> Variable.create!(name: "foo", type: "float", value: "5.34,43.4,10.0", array: true)
[2]> Variable.fetch("foo")
=> [5.34, 43.4, 10.0]
This is a very simple version of the Variable model. Another implementation could allow requesting multiple Variables at once, using a LIKE query for example.
Pros
- Fully Administered by Admin: Allows flexible management of configurations without developer intervention.
- Flexibility: Administrators can add, modify, or delete configurations on the fly.
Cons
- Risk of Errors: High risk of
human error, such as a typo in the variable name, which can lead to hard-to-detect bugs.
- Performance: Can impact performance if configurations are frequently queried without effective caching. This can quickly become a memory sink.
When to Use It?
I really like this approach for generalizing code aspects. It allows me to ignore the context beforehand and still develop quite advanced features. I also like that I often have to use a lot of meta-programming to exploit it to its full potential.
However, there are often errors because the admin or developer forgot to create or misspelled a variable.
TL;DR
If you're a bit lost with all these methods, I've prepared a simple questionnaire to help you decide when to use which method.
- Do configurations need to be modified?
- No: Constants
- Do administrators need to modify configuration variables without developer intervention?
- No: Configuration File
- Do administrators need full control over the configuration?
- Yes: Variable Model
- No: Configuration Model
Conclusion
In summary, choosing the right method to configure your application depends on the frequency of changes, who controls these changes, and the importance of flexibility.
Posted on August 1, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.