Design Rails enums the right way

Enum is a shortcut for the enumerated type, a data type consisting of a set of values. Rails provides enums in models, and the definition is straightforward:

class User < ApplicationRecord
  enum status: { invited: 0, active: 1 }
end

However, while it’s very easy to define enum and use all the dynamic methods provided by Rails, there are some things we should be aware of not to create data integrity problems and code that is hard to maintain.

This article is a set of good practices for enums in Rails. If you want to read more about using enums, check the latest article. You can also do a deep dive and see how enums work under the hood.

The right enum definition

There are two ways to define the enum in the model. The simpler way is to pass the values as an array:

class User < ApplicationRecord
  enum status: [:invited, :active]
end

And the second option is to pass hash with mappings:

class User < ApplicationRecord
  enum status: { invited: 0, active: 1 }
end

Both definitions are working the same way. However, it is recommended to always use hash because:

  • You know precisely what integers are mapped to what status
  • You can change the order of the keys, and it won’t break the data integrity, while with the array option, you will damage the data by changing the order

It’s a straightforward change from an array to hash but can save you a lot of time and prevent problems if someone accidentally changes the order of enum keys.

Enum with value object design pattern

If you plan to share logic between two or more models in case of the same enums or just add more logic to enums, it is a perfect idea to build the code with the value object design pattern.

A value object is a simple Ruby object that only returns values (it does not update any data). With the proper naming, such classes are easily readable and extendable pieces of code that every developer loves.

Let’s assume that you are building application tracking system and you have the candidate model with the following statuses defined:

class Candidate < ApplicationRecord
  enum status: {
    submitted: 0,
    viewed: 1,
    reviewed: 2,
    rejected: 3,
    invited: 4,
    interviewed: 5,
    verified: 6,
    offered: 7,
    hired: 8,
    in_trial: 9,
    after_trial: 10
  }
end

A lot of statuses. We would like to group them into the following groups:

  • in_process - submitted, viewed, reviewed, invited, interviewed, verified, and offered
  • employed - hired, in_trial, after_trail

Let’s create a class called CandidateStatus that would handle the mentioned statuses:

class CandidateStatus
  STATUSES = { submitted: 0, viewed: 1, reviewed: 2, rejected: 3, invited: 4, interviewed: 5,
    verified: 6, offered: 7, hired: 8, in_trial: 9, after_trial: 10
  }.freeze 

  EMPLOYED_STATUSES = %w(hired in_trial after_trial).freeze
  IN_PROCESS_STATUSES = %w(viewed reviewed rejected invited interviewed verified offered).freeze

  def initialize(status)
    @status = status
  end

  def to_s
    @status
  end

  def employed?
    EMPLOYED_STATUSES.include?(@status)
  end

  def in_process?
    IN_PROCESS_STATUSES.include?(@status)
  end
end

It’s effortless. We can now update the model to return the instance of CandidateStatus class each time the status method is called:

class Candidate < ApplicationRecord
  enum status: CandidateStatus::STATUSES

  def status
    @status ||= CandidateStatus.new(read_attribute(:status))
  end
end

With the above design we can now use meaningful methods to manage the candidate’s status:

candidate = Candidate.create(status: :reviewed)
candidate.reviewed? # => true
candidate.status.to_s # => "reviewed"
candidate.status.in_process? # => true

Our model won’t become fat, and the logic for statuses is separated in a very simple class that is easy to extend and test outside the model.

The right naming

Not always the naming convention that is available by default makes sense. Let’s consider the case where we have a model GraphicCard with the performance column:

class GraphicCard < ApplicationRecord
  enum performance: { low: 0, normal: 1, high: 2 }
end

Now we want to check if card has a high and low performance:

card = GraphicCard.create(performance: :low)
card.high? # => false
card.low? # => true

It does not make sense; what is card.low? ? To make the code more readable, we can define suffix and prefix. In our case suffix will do the work:

class GraphicCard < ApplicationRecord
  enum performance: { low: 0, normal: 1, high: 2 }, _suffix: true
end

With this configuration, every enum value is ended with “_performance” string:

card = GraphicCard.create(performance: :low)
card.high_performance? # => false
card.low_performance? # => true

Now, it makes sense! You know what this code is about even without seeing the enum definition. If you passed true either to _suffix or _prefix option, Rails would use the enum name (in our case performance), but you can also pass a string.

The next steps

The enum definition is simple, but it does not mean that it can’t lead to problems in the future. The right design will always make your code more extendable and readable.

If you are interested in more topics regarding enums, make sure you read the article about how enum are working under the hood