Question

How to Implement Enums in Ruby

rubyenums

Question

I want to implement an enum-style pattern in Ruby.

What is the best way to represent a fixed set of named values in Ruby so that I can use them similarly to Java or C# enums?

I am looking for an approach that is idiomatic in Ruby while still providing the clarity and safety of enums from statically typed languages.

Short Answer

By the end of this page, you will understand how Ruby handles enum-like values, why Ruby does not have Java-style enums built into the language in the same way, and which Ruby patterns are commonly used instead. You will learn when to use symbols, constants, custom classes, and framework features such as Rails enums, along with the trade-offs of each approach.

Concept

Ruby does not have a built-in enum feature that works exactly like Java or C# enums. Instead, Ruby developers usually model a fixed set of named values using simpler language features.

The core idea of an enum is this:

  • You have a limited list of allowed values.
  • Each value has a meaningful name.
  • You want your code to be readable and less error-prone.

For example, an order status might only allow:

  • :pending
  • :paid
  • :shipped
  • :cancelled

In Ruby, the most common enum-like choices are:

  • Symbols for lightweight named values
  • Constants for shared named values
  • Arrays or hashes to define allowed options
  • Custom classes/modules when you need richer behavior
  • Rails enum if you are using Active Record

Why this matters in real programming:

  • It prevents invalid values from being scattered through your code.
  • It makes conditions easier to read.
  • It centralizes business rules.
  • It reduces bugs caused by typos like "pendng" instead of "pending".

Ruby favors simple, flexible patterns over strict language-level enum syntax. That means the “best” enum approach depends on what you need:

  • If you only need named values, use symbols.
  • If you need validation, keep a list of allowed values.
  • If you need methods on each value, use a class-based approach.
  • If you are in Rails models, use enum.

So the Ruby way is usually not to imitate Java exactly, but to choose the lightest pattern that clearly expresses a fixed set of values.

Mental Model

Think of an enum as a menu with a fixed list of choices.

In Java or C#, the language gives you a special printed menu automatically. In Ruby, you usually create the menu yourself.

For example, imagine a coffee shop where drink sizes can only be:

  • small
  • medium
  • large

You could represent that menu in Ruby as:

  • symbols: :small, :medium, :large
  • constants: SMALL, MEDIUM, LARGE
  • a class if each size has extra behavior, like pricing rules

The important part is not the syntax. The important part is that your program treats the list as fixed and known in advance.

So an enum in Ruby is less like a special language feature and more like a well-organized set of allowed labels.

Take Quiz

Syntax and Examples

1. Simple enum-like values with symbols

This is the most idiomatic Ruby approach for many cases.

STATUSES = [:pending, :paid, :shipped, :cancelled].freeze

status = :paid

if STATUSES.include?(status)
  puts "Valid status"
else
  puts "Invalid status"
end

Explanation:

  • :paid is a symbol.
  • STATUSES stores the allowed values.
  • .freeze prevents accidental modification.
  • include? checks whether the value is allowed.

2. Using constants for named values

module Status
  PENDING = :pending
  PAID = :paid
  SHIPPED = :shipped
  CANCELLED = :cancelled

  ALL = [PENDING, PAID, SHIPPED, CANCELLED].freeze
end

status = Status::PAID
puts Status::ALL.include?(status)

This gives you a namespaced way to access the values, similar to enum members.

3. A class-based enum style

Use this when you want each enum value to be an object.

class Status
  attr_reader :name

  def initialize(name)
    @name = name
    freeze
  end

  PENDING = new("pending")
  PAID = new("paid")
  SHIPPED = new("shipped")
  CANCELLED = new("cancelled")

  ALL = [PENDING, PAID, SHIPPED, CANCELLED].freeze

  def to_s
    name
  end
end

status = Status::PAID
puts status
puts Status::ALL.include?(status)

This is closer to Java-style enums because each value is a distinct object.

4. Rails Active Record enum

If you are using Rails, there is built-in support for model enums.

class Order < ApplicationRecord
  enum :status, { pending: 0, paid: 1, shipped: 2, cancelled: 3 }
end

This gives you helpful methods such as:

order.paid!
order.paid?
Order.shipped

This is the best choice when the enum belongs to a database-backed model in Rails.

Step by Step Execution

Consider this example:

module Status
  PENDING = :pending
  PAID = :paid
  SHIPPED = :shipped

  ALL = [PENDING, PAID, SHIPPED].freeze
end

status = :paid

if Status::ALL.include?(status)
  puts "Accepted: #{status}"
else
  puts "Rejected"
end

Step by step:

  1. Ruby defines a module named Status.
  2. Inside the module, three constants are created:
    • Status::PENDING
    • Status::PAID
    • Status::SHIPPED
  3. ALL stores every valid status in a frozen array.
  4. The variable status is assigned the symbol :paid.
  5. Status::ALL.include?(status) checks whether :paid is inside the list.
  6. Since it is included, the condition is true.
  7. Ruby prints:
Accepted: paid

Now imagine this value instead:

status = :unknown

Then include? returns false, so the output becomes:

Rejected

This shows the main purpose of an enum-like pattern in Ruby: keeping values restricted to a known set.

Real World Use Cases

Enum-style patterns are common whenever a value must come from a small predefined set.

Common examples

  • Order status: :pending, :paid, :shipped
  • User roles: :admin, :editor, :viewer
  • Payment methods: :card, :bank_transfer, :cash
  • Job states: :queued, :running, :failed, :completed
  • Environment names: :development, :test, :production

In scripts

A command-line script might only accept certain modes:

MODES = [:fast, :safe, :debug].freeze

In APIs

A request parameter may only allow certain values:

SORT_OPTIONS = [:asc, :desc].freeze

In data processing

A file import tool may classify rows as:

  • :valid
  • :invalid
  • :duplicate

The enum-like structure helps ensure downstream logic handles only expected categories.

Take Quiz

Real Codebase Usage

In real Ruby codebases, developers often use enum-like patterns in a few practical ways.

1. Constants plus an ALL list

This is a common pattern for validation and clarity.

module Role
  ADMIN = :admin
  EDITOR = :editor
  VIEWER = :viewer

  ALL = [ADMIN, EDITOR, VIEWER].freeze
end

Why it is useful:

  • avoids magic values spread across files
  • gives one source of truth
  • works well in validations and forms

2. Guard clauses for validation

def set_status(status)
  raise ArgumentError, "Invalid status" unless Status::ALL.include?(status)

  @status = status
end

This is a common Ruby style: reject invalid input early.

3. Mapping enum values to labels or behavior

STATUS_LABELS = {
  pending: "Pending payment",
  paid: "Paid",
  shipped: "Shipped"
}.freeze

This keeps display text separate from program logic.

4. Using enums in Rails models

class User < ApplicationRecord
  enum :role, { viewer: 0, editor: 1, admin: 2 }
end

This is common when a model attribute has a fixed set of states.

5. Encapsulating behavior when the values need logic

If each enum value needs methods, teams often use objects rather than plain symbols.

class LogLevel
  attr_reader :name, :priority

  def initialize(name, priority)
    @name = name
    @priority = priority
    freeze
  end

  INFO = new("info", 1)
  WARN = new("warn", 2)
  ERROR = new("error", 3)
end

This is more verbose, but useful when values are more than labels.

Common Mistakes

1. Using plain strings everywhere

Broken example:

status = "Paid"

if status == "paid"
  puts "OK"
end

Problem:

  • string comparisons are easy to break because of case or spelling differences

Better:

status = :paid

2. Not centralizing allowed values

Broken example:

if status == :pending
  # ...
elsif status == :paid
  # ...
elsif status == :shipped
  # ...
end

Problem:

  • values are repeated throughout the codebase
  • adding a new value becomes harder

Better:

module Status
  PENDING = :pending
  PAID = :paid
  SHIPPED = :shipped
end

3. Forgetting to freeze shared collections

Broken example:

STATUSES = [:pending, :paid, :shipped]
STATUSES << :cancelled

Problem:

  • other code can accidentally change the enum list

Better:

STATUSES = [:pending, :paid, :shipped].freeze

4. Expecting Java-style enum behavior automatically

Some beginners expect features like:

  • strict type safety
  • built-in iteration over enum members
  • methods automatically attached to each enum member

Ruby does not provide that by default. You must choose a pattern that gives the features you need.

5. Overengineering a simple problem

If all you need is a fixed list of names, a full class-based enum may be too much.

Too complex for simple cases:

class Color
  RED = new("red")
  BLUE = new("blue")
end

Often simpler is better:

COLORS = [:red, :blue].freeze

6. Not validating external input

Broken example:

def create_order(status)
  @status = status
end

Problem:

  • any value can enter your system

Better:

def create_order(status)
  raise ArgumentError, "Invalid status" unless Status::ALL.include?(status)

  @status = status
end

Comparisons

ApproachBest forProsCons
Symbols in an arraySimple fixed valuesIdiomatic, short, easy to useNo extra behavior by default
Constants in a moduleNamed access like Status::PAIDClear namespacing, readableSlightly more setup
Custom class objectsValues with methods/dataClosest to Java-style enumsMore code, more complexity
StringsExternal input or serializationEasy to print/storeMore typo-prone in internal logic
Rails enumActive Record modelsBuilt-in query and helper methodsRails-specific

Symbols vs strings

FeatureSymbolsStrings
Typical for internal enum valuesYesLess ideal
Easy to mistypeLess visible but still possibleYes
Best for user input/outputUsually noYes
Common Ruby styleVery commonCommon at boundaries

Ruby enum patterns vs Java/C# enums

FeatureRuby patternsJava/C# enums
Built into languageUsually noYes
Strict type safetyLimitedStronger
FlexibilityHighModerate
Simplicity for small casesHighSometimes more formal
Object behavior per valuePossible with custom classesBuilt in
Take Quiz

Cheat Sheet

Quick reference

Simple enum-like list

STATUSES = [:pending, :paid, :shipped].freeze

Namespaced constants

module Status
  PENDING = :pending
  PAID = :paid
  SHIPPED = :shipped

  ALL = [PENDING, PAID, SHIPPED].freeze
end

Validate a value

Status::ALL.include?(status)

Raise on invalid input

raise ArgumentError, "Invalid status" unless Status::ALL.include?(status)

Class-based enum style

class Status
  PENDING = new("pending")
end

Use this only if values need behavior or data.

Rails enum

class Order < ApplicationRecord
  enum :status, { pending: 0, paid: 1, shipped: 2 }
end

Rules of thumb

  • Prefer symbols for internal enum-like values.
  • Use constants for readability and reuse.
  • Keep a single ALL list for validation.
  • Freeze shared arrays and hashes.
  • Use strings at system boundaries if needed, then convert.
  • Use a custom class only when you need more than labels.

Edge cases

  • External JSON or HTTP params usually arrive as strings.
  • Symbols and strings are different:
:paid == "paid" # false
  • Avoid scattered magic values like :pending in many files.
  • In Rails, prefer model-level enum when the value is stored in the database.

FAQ

Does Ruby have built-in enums like Java or C#?

Not in the same way. Ruby usually uses symbols, constants, arrays, hashes, or framework helpers such as Rails enum.

What is the most idiomatic enum-like approach in Ruby?

For simple cases, symbols are the most idiomatic choice, often combined with a frozen list of allowed values.

Should I use symbols or strings for enum values in Ruby?

Use symbols for internal logic in most cases. Use strings mainly when receiving or sending external data, such as JSON or HTTP parameters.

How can I validate enum values in Ruby?

Store all allowed values in a constant like ALL and use include? to check whether a value is valid.

How do I make Ruby enums look more like Java enums?

Use a module with constants or create a custom class where each enum value is a singleton-like object.

What is the best enum option in Rails?

If the enum is a model attribute stored in the database, use Active Record enum.

Can Ruby enum values have methods?

Yes, but you usually need a custom class or objects instead of plain symbols.

Why should I freeze enum arrays or hashes?

Freezing prevents accidental modification of the shared list of allowed values.

Take Quiz

Mini Project

Description

Build a small order status helper for an e-commerce application. The project demonstrates how to represent a fixed set of statuses in idiomatic Ruby, validate inputs, and convert those statuses into user-friendly labels. This is a practical example of how enum-like values are used in real code without needing Java-style language support.

Goal

Create a Ruby module and class that safely manage a fixed list of order statuses and reject invalid values.

Requirements

  • Define a fixed set of order statuses using Ruby constants and symbols.
  • Store all allowed statuses in one frozen collection.
  • Create an Order class that validates the status during initialization.
  • Add a method that returns a human-readable label for each status.
  • Show example usage with both valid and invalid statuses.
Take Quiz

Keep learning

Related questions

View more