Make your Rails App Configurable in 4 Ways

pimp_my_ruby

Pimp My Ruby

Posted on August 1, 2024

Make your Rails App Configurable in 4 Ways

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"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
  • 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

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.

  1. Do configurations need to be modified?
    • No: Constants
  2. Do administrators need to modify configuration variables without developer intervention?
    • No: Configuration File
  3. 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.

💖 💪 🙅 🚩
pimp_my_ruby
Pimp My Ruby

Posted on August 1, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related