Homepage

Ruby on Rails - How to Create Perfect Enum in 5 Steps

If you work on a project within the domain of Ruby on Rails, enum can be an attribute you’d want to understand and use frequently. Why is that? In short, enums (enumerations) make your work faster, more efficient and less erroneous.

When your project starts you probably design ERD diagram or a similar one. Then, each time when a client passes new requirements to you it’s necessary to modify it. That process helps to understand the specific domain and mirrors the reality. Entities that you model contain many attributes of various types. A quite popular requirement is to create an attribute that can be assigned to one of a few available values. In programming, that type is called enumeration or just enum.

As an example, that could be a type of delivery: "courier", "parcel station" or "personal". Rails supports enums from version 4.1.

This article consists of 3 sections:

  1. Basic solution - introduce ActiveRecord::Enum, as simple as possible

  2. 5 various steps to improve enums functioning

  3. Ultimate solution - wrap all improvements into one implementation

To better understand the topic let’s add some real background. In our recent project, we worked on a system related to artworks. Artworks were collected into Catalogs. The catalog was one of the biggest models in our app. Among many attributes, we had 4 of the enum type.

state: ["incoming", "in_progress", "finished"]

auction_type: ["traditional", "live", "internet"]

status: ["published", "unpublished", "not_set"]

localization: ["home", "foreign", "none"]

Basic Solution

Adding enum to an existing model is a really simple task. First of all, you need to create an appropriate migration. Notice that column type is set to integer and this is how Rails keeps enums values in the database.

rails g migration add_status_to_catalogs status:integer

tsx

Next step is to declare enum attribute in the model.

tsx

Run the migrations and that’s it! From now you can take advantage of the whole bunch of extra methods.

For example, you can check if the current status is set to a specific value:

catalog.published? # false

or change the status to other value:

tsx

List all published catalogs:

Catalog.published

To see all provided methods check ActiveRecord::Enum.

This simple solution is great for a start, but you may run into some troubles when your project will grow. To be prepared, you can implement a few improvements making your enums easier to maintain:

1. Declare enum as a Hash not Array

Vulnerability before change: Mapping between declared values and integers kept in the database is based on order in the array.

In this example mapping would be as follow:

tsx

That approach is not flexible at all. Imagine that requirements just have changed. From now "foreign" localization should be split into "America" and "Asia". In that case, you should remove old value and add two new ones. But... you cannot remove unused "foreign" type, because it violates an order of remaining values. To avoid this situation you should declare your enum as a Hash. There is not much to do:

tsx

This declaration doesn’t depend on an order so you will be able to implement the changes and get rid of unused enum value.

2. Integrate ActiveRecord::Enum with PostgreSQL enum

Vulnerability before change: Meaningless integer values in the database.

Working with attributes representing by integers in the database can be annoying. Imagine that you want to query something in rails console or even you need to create a scope based on your enum field. Backing to our background, let's suppose that we want to return all Catalogs which are still up to date. So we can write a where clause like this:

Catalog.where.not(“state = ?”, “finished”)

We got an error as we expected:

tsx

This problem occurs only in the array format of where clause because the second value is put directly into SQL where clause and obviously "finished" is not an integer.

A similar case can appear when you implement complex SQL query omitting ActiveRecord layer. When the query hasn’t access to the model then you lose meaningful information about values and stay with pure integers without sense. In that case, you need to put an extra effort to make these integers meaningful again.

Another annoying situation can take place when you work with a legacy database like this. You have access to the database and you are interested only in the data kept there. You are not able to get immediate information from what you see. Always need to map these numbers into real values from the domain.

It’s worth to remember that when integer enum is split from its model like in the examples above then we lose information, unfortunately.

To convince you even more, there is also safety point. When declaring ActiveRecord::Enum there is no guarantee that your data will be restricted only to provided values. Changes could be made by any SQL insertions. On the other hand, when you declare PostgreSQL enum you gain constraint on database level. You need to decide how confident you want to be.

PostgreSQL is commonly used as a database in Ruby on Rails projects. You can use PostgreSQL enum as a type of an attribute in the database table.

Let’s see how it looks this time.

rails g migration add_status_to_catalogs status:catalog_status

You need to change attribute type. I don’t recommend creating types like "status". It is likely that another status will appear in the future. Next, you need to change migration a little. Most of all it must be reversible and could execute SQL.

tsx

Declaration is similar to the previous one:

tsx

3. Add an index to enum attribute

Vulnerability before change: Queries performance.

This point is a simple one. It’s highly probable that your enum attribute is what can distinguish objects within a particular model. Like with our status: some Catalogs are published and others are not. As a consequence, searching or filtering by that attribute will be a quite frequent task, so it is worth to add an index to that field. Let's modify our migration:

tsx

4. Use prefix or suffix option in your enums

Vulnerabilities before change:

  • Unintuitive scopes

  • Bad readability of helper methods

  • Prone to errors

Referring to our recent project again, we had a few enums in our Catalog model:

state: ["incoming", "in_progress", "finished"]

auction_type: ["traditional", "live", "internet"]

status: ["published", "unpublished", "not_set"]

localization: ["home", "foreign", "none"]

To add prefix or suffix to enum it’s enough to add that option to the declaration, like so:

tsx

Let's see why it can be so helpful. In Catalog model, we have 4 enums and 12 values among them. It creates 12 scopes, very unintuitive scopes.

tsx

Can you say with ease what these methods return? No, you need to remember all the time how the scopes look. It may be annoying, really.

tsx

That looks much better.

Let's suppose now that you need to add one more enum to your model. It should keep information about the order of each catalog inside the global catalog. The order of some catalogs may not be specified. Most important is to know which one is first and which is last. We can create another enum:

tsx

Let's open rails console to test new scopes:

Catalog.order

We got an error. It’s self-explanatory.

tsx

Ok, we can fix it:

tsx

And again, another error:

tsx

Ok, that is obvious too. We forgot that value "none" was declared in another attribute as well.

Prefix or suffix options are a perfect fit to avoid these kinds of troubles. We can declare values just like we want, there is no reason to change words which are most descriptive. In that approach, scopes are more intuitive and meaningful. According to new attribute, the declaration should look like this:

tsx

5. Implement Value Object to handle an enum

Vulnerability before change: Fat model

I can recommend extracting enum attribute into separated Value Object in two cases:

  1. Enum attribute is used among many models (at least 2)

  2. Enum attribute has specific logic that complicates a model

Ok, let’s introduce sample situation. In our project auction houses (where artworks are sold) are placed all over the country. Poland divides into 16 regions, called voivodeships. Each AuctionHouse model has specific Address that contains Voivodeship attribute. You can imagine that for some reason there will be a necessity to list only northern auction houses or these from most popular voivodeships. In that case it is necessary to put extra logic into our model what makes it fatter. To avoid it you can extract that logic into another class making it reusable and cleaner.

tsx

Then in your corresponding model, you need to overwrite this attribute. In our project it is Address model. array_to_enum_hash is just helper method converting the Array of enum values into a Hash.

tsx

Here is what you achieved. Entire logic related to voivodeships is encapsulated into a single class. You can extend it as you want and Address model stayed thick.

Now, when you want to get voivodeship attribute, the object of Voivodeship class is returned. This is your Value Object.

tsx

Take a look that both voivodeships have the same value, but as objects, they aren’t equal. Thanks to our helper method we can check the equality in that way:

tsx

And what is most powerful you can take advantage of all defined methods that representing requirements we specified earlier.

tsx

Ultimate solution

Ok, you have passed through 5 enum improvements. Now it’s time to sum up all steps and create ultimate solution. As an example, let's take status attribute from Catalog model. Implementation can look like this:

Migration Generation:

rails g migration add_status_to_catalogs status:catalog_status

Migration:

tsx

ValueObject:

tsx

Catalog model & enum declaration:

tsx

Custom web app for live art auctions' market leader

Artinfo needed a seamless, real-time online auction platform to modernize bidding and engage more users. We built a scalable web platform that enhanced data processing, increased participation, and boosted sales.

75k

Unique users a month

80%

Of phone bids transferred online

300

Online bidding yearly

Learn moreLearn more about artinfo
Artinfo project case study card image

Conclusion

That’s all - 5 steps to build a better implementation of enums in Rails.

Sometimes all steps will be necessary and another time only a few of them. You can adjust this solution to your needs. Hope that you have found something useful in this article. Let me know in the comments what’s your opinions about enums. Maybe you can recommend any further improvements?

Let’s Create a Great Website Together

We'll shape your web platform the way you win it!

More posts in this category

  • February 05, 2025 • 10 min read

    API-first CMS: What Options You Have (Web Dev Agency's Take)

    According to the 2024 State of the API Report by Postman, 74% of organizations are adopting API-first strategies. This statistic isn’t just impressive—it signals a major shift in how businesses operate. While API-first approaches have long been a staple in software development, they're now reshaping content management as well. More and more companies are realizing that traditional CMS platforms can't keep up with the demand for flexibility, speed, and seamless integrations.

    READ MORELearn more about api first cms
  • January 23, 2025 • 15 min read

    Best CMS for SaaS: Top Cloud-Based Solutions

    Choosing the right Content Management System (CMS) is a critical decision for your SaaS business. Your unique needs require solutions that are not only flexible, scalable, and user-friendly but also tailored to meet the demands of a fast-paced, customer-focused industry. A CMS should simplify your workflows and help you deliver personalized, high-quality digital experiences.

    READ MORELearn more about best cms for saas top cloud based solutions
  • December 12, 2024 • 10 min read

    We Picked the Best (Headless) CMS for Vue

    Picture a digital experience where content effortlessly flows across platforms, development is agile, and performance is unmatched. By combining the power of Vue.js, a progressive JavaScript framework, with a modern headless CMS, you can achieve all this and more.

    READ MORELearn more about headless cms for vue