Constants #

Constants are Ruby’s way of storing values that shouldn’t change while the program runs — application configuration, mathematical values, lists of valid statuses, file size limits, and the like. But there’s something interesting and surprising about constants in Ruby: the interpreter doesn’t strictly forbid changing them. Ruby only issues a warning, then continues executing as if nothing happened. This means a “constant” in Ruby is better understood as a signal of intent to other programmers — not a technical guarantee that its value will never change. Understanding this nuance, along with how to create truly immutable constants, is the main topic of this article.

Defining Constants #

Constants in Ruby start with an uppercase letter. The community convention uses SCREAMING_SNAKE_CASE — all caps with underscores separating words — to distinguish them visually from variables and class names.

# Simple constants at the top level
PI            = 3.14159265358979
GRAVITY       = 9.80665           # m/s², Earth's standard gravity
SPEED_OF_LIGHT = 299_792_458      # meters per second

# Application configuration constants
APP_VERSION   = "2.4.1"
APP_NAME      = "Inventory Pro"
UPLOAD_LIMIT  = 10 * 1024 * 1024  # 10 MB in bytes
TIMEOUT_SECONDS = 30

# Constants holding collections
VALID_STATUS  = [:active, :inactive, :pending, :blocked].freeze
IMAGE_FORMATS = %w[jpg jpeg png webp gif].freeze

Class and module names are also constants — they just use PascalCase instead of SCREAMING_SNAKE_CASE.

# These are also constants — classes and modules are always constants in Ruby
class PaymentProcessor    # PascalCase — a constant of class type
end

module AuthHelper          # PascalCase — a constant of module type
end

# Check with constants — lists all defined constants
puts Object.constants.grep(/^[A-Z_]+$/).first(5).inspect
# => [:PI, :GRAVITY, :APP_VERSION, ...]

Reassignment Behavior — Warning, Not Error #

This is the most important part to understand: Ruby does not forbid constant reassignment. All it does is print a warning to stderr, then continue executing with the new value.

LIMIT = 100
puts LIMIT   # => 100

LIMIT = 200  # warning: already initialized constant LIMIT
             # warning: previous definition of LIMIT was here
puts LIMIT   # => 200  (the change still succeeded!)

This means a “constant” in Ruby isn’t an immutability guarantee — it’s a communication convention. When you write MAX_LIMIT = 50, you’re telling other developers: “this value shouldn’t be changed, think twice before modifying it.”

# ANTI-PATTERN: changing a constant mid-program
HOLIDAY_DISCOUNT = 0.3

def calculate_discount(price)
  price * HOLIDAY_DISCOUNT
end

# Elsewhere in the code:
HOLIDAY_DISCOUNT = 0.5   # ← warning, but still works — very dangerous!
# Now all already-running calculations use a different value

# CORRECT: if the value needs to change, use a variable or method, not a constant
def active_discount
  ENV["DISCOUNT_PCT"]&.to_f || 0.3
end
If you run with the -W2 option or enable verbose warnings, Ruby will show the exact location of both the original definition and the constant reassignment. In production environments, some frameworks like Rails configure Ruby to treat these warnings as serious signals. Don’t ignore already initialized constant warnings — they almost always indicate a bug or a design issue that needs fixing.

Mutation vs Reassignment #

There’s an important distinction that often confuses people: reassignment (replacing the object a constant refers to) produces a warning, but mutation (changing the contents of the referenced object) produces no warning at all.

DESTINATION_CITIES = ["Jakarta", "Surabaya", "Bandung"]

# Reassignment — warning
DESTINATION_CITIES = ["Medan", "Makassar"]   # ← warning: already initialized constant

# Mutation — NO warning, but the constant's value changes!
DESTINATION_CITIES << "Yogyakarta"           # ← no warning
DESTINATION_CITIES.push("Semarang")          # ← no warning
puts DESTINATION_CITIES.inspect
# => ["Jakarta", "Surabaya", "Bandung", "Yogyakarta", "Semarang"]

This is why freeze is so important for constants that are collections (Array, Hash, String):

# ANTI-PATTERN: collection constant without freeze — can be silently mutated
ORDER_STATUS = ["pending", "processing", "shipped", "completed"]
ORDER_STATUS << "cancelled"   # no warning, but this changes the "constant"!

# CORRECT: freeze prevents mutation
ORDER_STATUS = ["pending", "processing", "shipped", "completed"].freeze
ORDER_STATUS << "cancelled"   # => FrozenError: can't modify frozen Array

# Hashes also need freezing
HTTP_CODES = {
  ok:           200,
  not_found:    404,
  server_error: 500
}.freeze

HTTP_CODES[:created] = 201   # => FrozenError: can't modify frozen Hash
flowchart TD
    A[Constant refers to an object] --> B{What are you doing?}
    B --> C["Reassignment\nCONSTANT = new_value"]
    B --> D["Mutation\nCONSTANT << element"]
    C --> E["Ruby warning\n(still works)"]
    D --> F{Is the object frozen?}
    F --> G["Yes — freeze active\n→ FrozenError"]
    F --> H["No — without freeze\n→ Succeeds without warning!"]
    G --> I["✓ Constant protected"]
    H --> J["✗ Value changes silently"]

Deep Freeze for Nested Collections #

freeze only freezes the object at the surface — its elements can still be mutated if they’re mutable objects.

# Shallow freeze — string elements inside the array can still be mutated
DAY_NAMES = ["Monday", "Tuesday", "Wednesday"].freeze
DAY_NAMES << "Thursday"        # => FrozenError: the array is frozen
DAY_NAMES[0] << " Morning"     # SUCCEEDS! The String inside isn't frozen
puts DAY_NAMES[0]              # => "Monday Morning"  ← value changed!

# CORRECT: freeze each element too
DAY_NAMES = ["Monday".freeze, "Tuesday".freeze, "Wednesday".freeze].freeze
DAY_NAMES[0] << " Morning"     # => FrozenError: the string is frozen too

# A more concise way using map:
DAY_NAMES = ["Monday", "Tuesday", "Wednesday"].map(&:freeze).freeze

# Or with the magic comment — freezes all string literals in the file:
# frozen_string_literal: true
DAY_NAMES = ["Monday", "Tuesday", "Wednesday"].freeze
DAY_NAMES[0] << " Morning"     # => FrozenError because of frozen_string_literal

Constants in Classes and Modules #

Defining constants inside a class or module is a very common and recommended practice. It provides namespacing — constants are bound to their relevant context instead of floating in the global scope.

class DatabaseConfig
  HOST      = ENV.fetch("DB_HOST", "localhost")
  PORT      = ENV.fetch("DB_PORT", "5432").to_i
  DB_NAME   = ENV.fetch("DB_NAME", "app_development")
  POOL_SIZE = 10
  TIMEOUT   = 5_000   # milliseconds

  def self.connection_string
    "postgresql://#{HOST}:#{PORT}/#{DB_NAME}"
  end
end

puts DatabaseConfig::HOST          # => "localhost"
puts DatabaseConfig::PORT          # => 5432
puts DatabaseConfig.connection_string
# => "postgresql://localhost:5432/app_development"
module InputValidation
  MIN_PASSWORD_LENGTH = 8
  MAX_PASSWORD_LENGTH = 128
  EMAIL_PATTERN   = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i.freeze
  PHONE_PATTERN   = /\A(\+62|0)[0-9]{9,12}\z/.freeze
  SPECIAL_CHARS   = %w[! @ # $ % ^ & * ( )].freeze

  def self.email_valid?(email)
    email.match?(EMAIL_PATTERN)
  end

  def self.password_valid?(password)
    password.length.between?(MIN_PASSWORD_LENGTH, MAX_PASSWORD_LENGTH)
  end
end

puts InputValidation.email_valid?("[email protected]")   # => true
puts InputValidation.email_valid?("not-an-email")       # => false
puts InputValidation::MIN_PASSWORD_LENGTH               # => 8

Constants in Subclasses #

Constants are inherited by subclasses — a subclass can access its parent class’s constants. But a subclass can also redefine a constant with its own value without affecting the parent class.

class Animal
  BIOLOGICAL_KINGDOM = "Animalia"
  NORMAL_TEMP        = 37.0   # Celsius, mammalian average

  def basic_info
    "Kingdom: #{BIOLOGICAL_KINGDOM}, Normal temp: #{NORMAL_TEMP}°C"
  end
end

class Reptile < Animal
  NORMAL_TEMP = nil   # reptiles are cold-blooded — override the parent constant

  def basic_info
    # BIOLOGICAL_KINGDOM is inherited from Animal
    "Kingdom: #{BIOLOGICAL_KINGDOM}, Cold-blooded"
  end
end

puts Animal.new.basic_info    # => Kingdom: Animalia, Normal temp: 37.0°C
puts Reptile.new.basic_info   # => Kingdom: Animalia, Cold-blooded

puts Animal::NORMAL_TEMP      # => 37.0   (unchanged)
puts Reptile::NORMAL_TEMP     # => nil

The Constant Lookup Path #

When Ruby encounters a name starting with an uppercase letter in code, it searches for that constant through the constant lookup path — a sequence of scopes checked one by one.

LEVEL = "global"

module Outer
  LEVEL = "outer module"

  module Inner
    LEVEL = "inner module"

    def self.print_level
      puts LEVEL          # => "inner module" — found in the nearest scope
    end
  end

  def self.print_level
    puts LEVEL            # => "outer module"
    puts Inner::LEVEL     # => "inner module" — explicit access
    puts ::LEVEL          # => "global" — :: forces a top-level lookup
  end
end

Outer.print_level
Outer::Inner.print_level
flowchart TD
    A["Ruby encounters the name CONSTANT"] --> B["Search in the current\nclass/module scope"]
    B --> C{Found?}
    C --> |Yes| Z["Use this value"]
    C --> |No| D["Search in included/\nextended modules"]
    D --> E{Found?}
    E --> |Yes| Z
    E --> |No| F["Search in parent\nclasses (ancestor chain)"]
    F --> G{Found?}
    G --> |Yes| Z
    G --> |No| H["Search at the top level\n(Object)"]
    H --> I{Found?}
    I --> |Yes| Z
    I --> |No| J["NameError: uninitialized\nconstant CONSTANT"]

The :: operator is used for explicit namespace navigation:

# Access constants from a specific namespace
puts Math::PI           # => 3.141592653589793
puts Math::E            # => 2.718281828459045

# :: at the start means top-level
module App
  VERSION = "1.0"

  class Config
    VERSION = "config-1.0"

    def app_version
      ::App::VERSION    # access App::VERSION from inside Config
    end

    def config_version
      VERSION           # local constant — Config::VERSION
    end
  end
end

c = App::Config.new
puts c.app_version     # => "1.0"
puts c.config_version  # => "config-1.0"

Constant Usage Patterns in Real Applications #

Here are some common constant patterns you’ll encounter in professional Ruby and Rails codebases.

Manual Enums with Hashes #

Before Ruby had a built-in enum, developers used constant Hashes to define limited value sets.

module OrderStatus
  PENDING   = "pending"
  PROCESSING = "processing"
  SHIPPED   = "shipped"
  COMPLETED = "completed"
  CANCELLED = "cancelled"

  ALL = [PENDING, PROCESSING, SHIPPED, COMPLETED, CANCELLED].freeze

  CAN_BE_CANCELLED = [PENDING, PROCESSING].freeze
  IS_FINAL         = [COMPLETED, CANCELLED].freeze

  def self.valid?(status)
    ALL.include?(status)
  end

  def self.cancellable?(status)
    CAN_BE_CANCELLED.include?(status)
  end
end

order_status = "shipped"
puts OrderStatus.valid?(order_status)            # => true
puts OrderStatus.cancellable?(order_status)      # => false

# Usage in other code — safer than string literals
if order.status == OrderStatus::COMPLETED
  send_confirmation_email(order)
end

Nested Configuration with Modules #

module Config
  module Database
    ADAPTER  = "postgresql"
    POOL     = Integer(ENV.fetch("DB_POOL", 5))
    TIMEOUT  = 5_000
  end

  module Cache
    DRIVER   = :redis
    TTL      = 3_600          # 1 hour in seconds
    MAX_SIZE = 256 * 1024 * 1024  # 256 MB
  end

  module Upload
    MAX_SIZE      = 10 * 1024 * 1024   # 10 MB
    ALLOWED_TYPES = %w[jpg jpeg png pdf docx].freeze
    TEMP_PATH     = "/tmp/uploads".freeze
  end
end

puts Config::Database::POOL          # => 5
puts Config::Upload::MAX_SIZE        # => 10485760
puts Config::Upload::ALLOWED_TYPES.inspect
# => ["jpg", "jpeg", "png", "pdf", "docx"]

Environment-Based Constants #

Constants whose values are read from environment variables are a common pattern for configuration that differs between environments (development, staging, production).

module AppConfig
  # ENV.fetch raises KeyError if the variable is missing (safer than ENV[])
  SECRET_KEY   = ENV.fetch("SECRET_KEY_BASE") { raise "SECRET_KEY_BASE must be set!" }
  DATABASE_URL = ENV.fetch("DATABASE_URL", "postgresql://localhost/app_dev")

  # Boolean values from the environment
  DEBUG_MODE   = ENV.fetch("DEBUG", "false") == "true"
  MAINTENANCE  = ENV.fetch("MAINTENANCE_MODE", "false") == "true"

  # Numeric values — always convert types explicitly
  MAX_WORKERS  = Integer(ENV.fetch("MAX_WORKERS", 4))
  REQUEST_TIMEOUT = Float(ENV.fetch("REQUEST_TIMEOUT", 30.0))
end
# ANTI-PATTERN: using ENV[] directly all over the code
# (no single point of control, no type guarantees)
def send_email
  smtp_host = ENV["SMTP_HOST"]   # could be nil anytime, String or nil type
  # ...
end

# CORRECT: centralize in constants with guaranteed types
module Mailer
  SMTP_HOST = ENV.fetch("SMTP_HOST", "localhost").freeze
  SMTP_PORT = Integer(ENV.fetch("SMTP_PORT", 587))
  FROM_ADDR = ENV.fetch("MAIL_FROM", "[email protected]").freeze
end

def send_email
  smtp_host = Mailer::SMTP_HOST   # always a String, never nil
  # ...
end

Constant Naming Conventions #

Ruby uses two styles for constants, and both have their rightful place.

StyleWhen to useExample
SCREAMING_SNAKE_CASEConfiguration values, numbers, strings, collectionsMAX_SIZE, DEFAULT_TIMEOUT, ACTIVE_STATUS
PascalCaseClass, module, and type namesUserService, AuthHelper, HttpClient
# SCREAMING_SNAKE_CASE — for configuration values and literals
MAX_ATTEMPTS  = 3
PAGE_LIMIT    = 25
EXTERNAL_API_URL = "https://api.example.com/v2".freeze

# PascalCase — for classes and modules (also constants!)
class PaymentManager
  ACTIVE_PROVIDER = :midtrans.freeze   # config constant inside a class
end

module FormattingUtils
  DEFAULT_LOCALE = :id.freeze
end

# ANTI-PATTERN: inconsistent styles
maxSize = 10        # ← this isn't a constant, it's a local variable!
Max_Size = 10       # ← valid but not idiomatic
MAXSIZE = 10        # ← valid but hard to read

Constant Introspection #

Ruby provides several methods to explore defined constants at runtime — useful for debugging and metaprogramming.

module Configuration
  VERSION   = "1.0"
  DEBUG     = false
  TIMEOUT   = 30
end

# List all constants in a module
puts Configuration.constants.inspect
# => [:VERSION, :DEBUG, :TIMEOUT]

# Fetch a constant's value dynamically
constant_name = :VERSION
puts Configuration.const_get(constant_name)   # => "1.0"

# Check whether a constant exists
puts Configuration.const_defined?(:VERSION)      # => true
puts Configuration.const_defined?(:NOT_THERE)    # => false

# Set a constant dynamically (use with caution)
Configuration.const_set(:ENVIRONMENT, "production")
puts Configuration::ENVIRONMENT   # => "production"
When to use constant introspection:
  ✓ Loading configuration dynamically from a file or database
  ✓ Metaprogramming — generating classes or constants automatically
  ✓ Testing — verifying expected constants exist
  ✓ Debugging — seeing all constants defined in a module

  ✗ Don't use const_set to change an already-existing constant
  ✗ Avoid const_get with user input — potential security issue

Summary #

  • Constants start with an uppercase letterSCREAMING_SNAKE_CASE for configuration values, PascalCase for classes and modules.
  • Ruby doesn’t forbid reassignment — it only warns — a constant is a signal of intent, not a technical guarantee. Don’t ignore already initialized constant warnings.
  • Mutation is different from reassignment — changing the contents of a constant collection produces no warning, so it can happen silently without detection.
  • Always freeze collection constants — Arrays, Hashes, and Strings used as constant values should be freezed to prevent accidental mutation.
  • freeze is only shallow — for nested collections, also freeze each element inside, or enable # frozen_string_literal: true.
  • Define constants inside classes or modules — this provides clear namespacing and prevents global scope pollution.
  • :: for explicit namespace navigationApp::Config::TIMEOUT is clearer than implicit access relying on the lookup path.
  • Centralize environment configuration — read ENV in one place and store it as clearly typed constants, instead of calling ENV[] all over the place.
  • const_get and const_defined? for dynamically introspecting constants when needed in metaprogramming or testing.

← Previous: Variables   Next: Data Types →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact