CrackedRuby logo

CrackedRuby

Configuration Management

Overview

Configuration management in Ruby centers on organizing, storing, and accessing application settings through multiple approaches. Ruby provides several mechanisms for configuration: constants for immutable values, class and instance variables for runtime modification, environment variables for deployment flexibility, and external files for complex structured data.

The core Ruby approach uses constants defined at the module or class level for configuration that remains unchanged during execution. Class variables (@@variable) and class instance variables (@variable defined at class level) handle configuration that changes at runtime. Environment variables accessed through ENV provide deployment-specific configuration, while file-based approaches using YAML, JSON, or Ruby files handle complex hierarchical settings.

# Constants for immutable configuration
module Config
  DATABASE_TIMEOUT = 30
  MAX_RETRIES = 3
end

# Class variables for runtime configuration  
class ApiClient
  @@base_url = 'https://api.example.com'
  @@timeout = 10
  
  def self.configure(base_url: nil, timeout: nil)
    @@base_url = base_url if base_url
    @@timeout = timeout if timeout
  end
end

# Environment variable configuration
database_url = ENV['DATABASE_URL'] || 'postgres://localhost/app'
debug_mode = ENV['DEBUG'] == 'true'

Ruby's configuration patterns support both compile-time and runtime modification. Constants provide immutable configuration loaded when the class loads, while class variables and methods enable dynamic reconfiguration. Environment variables bridge the gap between application code and deployment environments, following the twelve-factor app methodology.

File-based configuration typically uses YAML for human-readable structured data, JSON for programmatic configuration, or Ruby files for executable configuration with conditional logic. Each approach offers different trade-offs between readability, flexibility, and security.

Basic Usage

Ruby configuration management begins with choosing the appropriate storage mechanism based on when configuration changes and who controls the values. Constants work for values determined at application design time, class variables for values that change during application runtime, and environment variables for values that change between deployments.

Constants provide the simplest configuration approach. Define constants in modules to create namespaced configuration accessible throughout the application:

module DatabaseConfig
  HOST = 'localhost'
  PORT = 5432
  POOL_SIZE = 20
  TIMEOUT = 30
end

# Access configuration
connection = Database.connect(
  host: DatabaseConfig::HOST,
  port: DatabaseConfig::PORT,
  pool_size: DatabaseConfig::POOL_SIZE
)

Class variables enable runtime configuration changes. Use class methods to modify configuration and instance methods to access current values:

class Logger
  @@level = :info
  @@output = STDOUT
  
  def self.configure
    yield(self) if block_given?
  end
  
  def self.level=(new_level)
    @@level = new_level
  end
  
  def self.level
    @@level
  end
  
  def self.output=(new_output)
    @@output = new_output
  end
  
  def log(message, level = :info)
    return unless should_log?(level)
    @@output.puts "[#{level.upcase}] #{message}"
  end
  
  private
  
  def should_log?(level)
    level_priority(level) >= level_priority(@@level)
  end
  
  def level_priority(level)
    { debug: 0, info: 1, warn: 2, error: 3 }[level] || 1
  end
end

# Configure at runtime
Logger.configure do |config|
  config.level = :debug
  config.output = File.open('app.log', 'a')
end

Environment variables provide deployment-specific configuration without modifying code. Ruby accesses environment variables through the ENV hash, with common patterns for default values and type conversion:

class AppConfig
  def self.database_url
    ENV['DATABASE_URL'] || 'postgres://localhost/development'
  end
  
  def self.port
    (ENV['PORT'] || '3000').to_i
  end
  
  def self.debug_mode?
    ENV['DEBUG'].to_s.downcase == 'true'
  end
  
  def self.worker_count
    ENV['WORKER_COUNT']&.to_i || processor_count
  end
  
  private
  
  def self.processor_count
    `nproc`.to_i
  rescue
    4
  end
end

# Usage
server = WebServer.new(port: AppConfig.port)
server.enable_debug if AppConfig.debug_mode?

File-based configuration handles complex structured data. YAML provides human-readable configuration with Ruby's built-in YAML module:

require 'yaml'

class FileConfig
  def self.load(file_path)
    @config = YAML.load_file(file_path)
  rescue Errno::ENOENT
    raise "Configuration file not found: #{file_path}"
  rescue Psych::SyntaxError => e
    raise "Invalid YAML in configuration file: #{e.message}"
  end
  
  def self.get(key_path)
    keys = key_path.split('.')
    keys.reduce(@config) { |config, key| config&.dig(key.to_s) }
  end
  
  def self.[](key)
    @config[key.to_s]
  end
end

# config/application.yml
# database:
#   host: localhost
#   port: 5432
#   pool_size: 20
# logging:
#   level: info
#   file: log/application.log

FileConfig.load('config/application.yml')
db_host = FileConfig.get('database.host')
log_level = FileConfig.get('logging.level')

Advanced Usage

Advanced configuration management in Ruby involves dynamic configuration loading, nested configuration hierarchies, configuration inheritance, and runtime reconfiguration with validation. These patterns handle complex applications with multiple environments, feature flags, and configuration dependencies.

Dynamic configuration loading creates configuration objects that respond to method calls with configuration values. This pattern uses method_missing to provide dot notation access to nested configuration:

class DynamicConfig
  def initialize(data = {})
    @data = data.transform_keys(&:to_s)
  end
  
  def self.load_from_file(file_path)
    data = YAML.load_file(file_path)
    new(data)
  end
  
  def method_missing(method_name, *args, &block)
    key = method_name.to_s
    
    if key.end_with?('=')
      # Setter method
      actual_key = key.chop
      @data[actual_key] = args.first
    elsif @data.key?(key)
      value = @data[key]
      # Return nested DynamicConfig for Hash values
      value.is_a?(Hash) ? DynamicConfig.new(value) : value
    else
      super
    end
  end
  
  def respond_to_missing?(method_name, include_private = false)
    key = method_name.to_s
    key.end_with?('=') || @data.key?(key.to_s) || super
  end
  
  def to_h
    @data.dup
  end
  
  def merge(other_config)
    merged_data = deep_merge(@data, other_config.to_h)
    DynamicConfig.new(merged_data)
  end
  
  private
  
  def deep_merge(hash1, hash2)
    hash1.merge(hash2) do |key, old_val, new_val|
      if old_val.is_a?(Hash) && new_val.is_a?(Hash)
        deep_merge(old_val, new_val)
      else
        new_val
      end
    end
  end
end

# Usage with nested access
config = DynamicConfig.load_from_file('config/app.yml')
puts config.database.host
puts config.redis.timeout
config.feature_flags.new_ui = true

Configuration inheritance handles different environments and overrides through a hierarchy system. Base configuration provides defaults while environment-specific configuration overrides specific values:

class EnvironmentConfig
  def initialize(base_config = {})
    @base_config = base_config
    @environment_configs = {}
    @current_environment = ENV['RAILS_ENV'] || 'development'
  end
  
  def load_environment(env_name, config_data)
    @environment_configs[env_name.to_s] = config_data
  end
  
  def load_from_directory(config_dir)
    base_file = File.join(config_dir, 'base.yml')
    @base_config = YAML.load_file(base_file) if File.exist?(base_file)
    
    Dir.glob(File.join(config_dir, '*.yml')).each do |file|
      next if File.basename(file, '.yml') == 'base'
      
      env_name = File.basename(file, '.yml')
      env_config = YAML.load_file(file)
      load_environment(env_name, env_config)
    end
  end
  
  def current_config
    @current_config ||= build_config_for(@current_environment)
  end
  
  def config_for(environment)
    build_config_for(environment.to_s)
  end
  
  def method_missing(method_name, *args, &block)
    current_config.send(method_name, *args, &block)
  end
  
  private
  
  def build_config_for(environment)
    env_config = @environment_configs[environment] || {}
    merged_config = deep_merge(@base_config, env_config)
    DynamicConfig.new(merged_config)
  end
  
  def deep_merge(hash1, hash2)
    hash1.merge(hash2) do |key, old_val, new_val|
      if old_val.is_a?(Hash) && new_val.is_a?(Hash)
        deep_merge(old_val, new_val)
      else
        new_val
      end
    end
  end
end

# Directory structure:
# config/
#   base.yml
#   development.yml  
#   staging.yml
#   production.yml

config = EnvironmentConfig.new
config.load_from_directory('config')
puts config.database.host  # Uses current environment
puts config.config_for('production').database.host  # Specific environment

Runtime configuration validation ensures configuration values meet application requirements. This pattern validates configuration on access and provides helpful error messages:

class ValidatedConfig
  ConfigError = Class.new(StandardError)
  
  def initialize(data = {})
    @data = data
    @validators = {}
    @required_keys = Set.new
  end
  
  def validate(key, &validator_block)
    @validators[key.to_s] = validator_block
    self
  end
  
  def require_key(key)
    @required_keys.add(key.to_s)
    self
  end
  
  def get(key)
    key_str = key.to_s
    
    # Check if required key exists
    if @required_keys.include?(key_str) && !@data.key?(key_str)
      raise ConfigError, "Required configuration key missing: #{key}"
    end
    
    value = @data[key_str]
    
    # Validate value if validator exists
    if @validators.key?(key_str)
      validator = @validators[key_str]
      begin
        validator.call(value)
      rescue StandardError => e
        raise ConfigError, "Configuration validation failed for #{key}: #{e.message}"
      end
    end
    
    value
  end
  
  def []=(key, value)
    @data[key.to_s] = value
  end
  
  def validate_all
    @required_keys.each { |key| get(key) }
    
    @data.keys.each do |key|
      get(key) if @validators.key?(key)
    end
    
    true
  end
end

# Configuration with validation
config = ValidatedConfig.new

config.require_key('database_url')
      .validate('database_url') { |url| raise 'Invalid URL' unless url =~ /\A\w+:\/\// }

config.require_key('port')
      .validate('port') { |port| raise 'Port must be 1-65535' unless (1..65535).include?(port) }

config.validate('timeout') { |t| raise 'Timeout must be positive' unless t > 0 }

config['database_url'] = 'postgres://localhost/app'
config['port'] = 3000
config['timeout'] = 30

config.validate_all  # Validates all configuration
database_url = config.get('database_url')  # Validates on access

Error Handling & Debugging

Configuration management error handling focuses on missing configuration files, invalid values, type conversion failures, and environment variable parsing problems. Ruby configuration systems must handle these errors gracefully while providing useful debugging information for developers and operators.

Missing configuration files represent the most common configuration error. Applications should provide clear error messages and fallback behavior when configuration files cannot be loaded:

class ConfigLoader
  ConfigurationError = Class.new(StandardError)
  FileNotFoundError = Class.new(ConfigurationError)
  ParseError = Class.new(ConfigurationError)
  
  def self.load_yaml(file_path, required: true)
    YAML.load_file(file_path)
  rescue Errno::ENOENT
    if required
      raise FileNotFoundError, "Required configuration file not found: #{file_path}"
    else
      puts "Warning: Optional configuration file not found: #{file_path}"
      {}
    end
  rescue Psych::SyntaxError => e
    raise ParseError, "YAML parsing error in #{file_path}: #{e.message}"
  rescue StandardError => e
    raise ConfigurationError, "Failed to load configuration from #{file_path}: #{e.message}"
  end
  
  def self.load_with_fallback(primary_path, fallback_path = nil)
    load_yaml(primary_path, required: false).tap do |config|
      if config.empty? && fallback_path
        return load_yaml(fallback_path, required: true)
      elsif config.empty?
        raise FileNotFoundError, "No configuration file found at #{primary_path}"
      end
    end
  end
  
  def self.load_multiple(file_paths)
    configs = []
    errors = []
    
    file_paths.each do |path|
      begin
        configs << load_yaml(path, required: false)
      rescue ConfigurationError => e
        errors << e
      end
    end
    
    if configs.all?(&:empty?)
      error_messages = errors.map(&:message).join('; ')
      raise FileNotFoundError, "No valid configuration files found. Errors: #{error_messages}"
    end
    
    configs.reduce({}) { |merged, config| merged.merge(config) }
  end
end

# Usage with error handling
begin
  config = ConfigLoader.load_with_fallback(
    "config/#{ENV['RAILS_ENV']}.yml",
    'config/development.yml'
  )
rescue ConfigLoader::FileNotFoundError => e
  puts "Configuration error: #{e.message}"
  puts "Create a configuration file or set default values"
  exit 1
rescue ConfigLoader::ParseError => e
  puts "Configuration parsing error: #{e.message}"
  puts "Check YAML syntax in configuration file"
  exit 1
end

Environment variable parsing requires careful type conversion and validation. Environment variables always arrive as strings, requiring conversion to appropriate Ruby types with error handling:

class EnvConfig
  ConversionError = Class.new(StandardError)
  
  def self.string(key, default: nil)
    ENV[key] || default
  end
  
  def self.integer(key, default: nil)
    value = ENV[key]
    return default unless value
    
    Integer(value)
  rescue ArgumentError
    raise ConversionError, "Cannot convert ENV['#{key}'] = '#{value}' to integer"
  end
  
  def self.float(key, default: nil)
    value = ENV[key]
    return default unless value
    
    Float(value)
  rescue ArgumentError
    raise ConversionError, "Cannot convert ENV['#{key}'] = '#{value}' to float"
  end
  
  def self.boolean(key, default: nil)
    value = ENV[key]
    return default unless value
    
    case value.downcase
    when 'true', '1', 'yes', 'on'
      true
    when 'false', '0', 'no', 'off'
      false
    else
      raise ConversionError, "Cannot convert ENV['#{key}'] = '#{value}' to boolean. Use true/false, 1/0, yes/no, or on/off"
    end
  end
  
  def self.array(key, delimiter: ',', default: nil)
    value = ENV[key]
    return default unless value
    
    value.split(delimiter).map(&:strip)
  end
  
  def self.required(key, type: :string)
    value = send(type, key)
    return value unless value.nil?
    
    raise ConversionError, "Required environment variable ENV['#{key}'] is not set"
  end
  
  def self.validate_presence(*keys)
    missing_keys = keys.select { |key| ENV[key].nil? || ENV[key].empty? }
    return if missing_keys.empty?
    
    raise ConversionError, "Missing required environment variables: #{missing_keys.join(', ')}"
  end
end

# Usage with comprehensive error handling
begin
  EnvConfig.validate_presence('DATABASE_URL', 'SECRET_KEY')
  
  config = {
    database_url: EnvConfig.required('DATABASE_URL'),
    port: EnvConfig.integer('PORT', default: 3000),
    debug: EnvConfig.boolean('DEBUG', default: false),
    worker_count: EnvConfig.integer('WORKERS', default: 2),
    allowed_hosts: EnvConfig.array('ALLOWED_HOSTS', default: ['localhost'])
  }
rescue EnvConfig::ConversionError => e
  puts "Environment configuration error: #{e.message}"
  puts "Check your environment variable settings"
  exit 1
end

Configuration debugging requires visibility into configuration loading, merging, and resolution. A debugging-enabled configuration class provides detailed information about configuration sources and values:

class DebuggableConfig
  def initialize(debug: false)
    @debug = debug
    @config_sources = {}
    @access_log = []
    @final_config = {}
  end
  
  def load_from_source(source_name, data)
    log_debug "Loading configuration from #{source_name}"
    log_debug "Data: #{data.inspect}"
    
    @config_sources[source_name] = data
    rebuild_final_config
  end
  
  def get(key)
    value = @final_config[key.to_s]
    source = find_value_source(key.to_s)
    
    @access_log << {
      key: key,
      value: value,
      source: source,
      timestamp: Time.now
    }
    
    log_debug "Accessing #{key} = #{value.inspect} (from #{source})"
    value
  end
  
  def debug_info(key = nil)
    if key
      key_info(key.to_s)
    else
      full_debug_info
    end
  end
  
  def access_history
    @access_log.dup
  end
  
  private
  
  def rebuild_final_config
    @final_config = {}
    
    @config_sources.each do |source_name, data|
      log_debug "Merging #{source_name}: #{data.keys.join(', ')}"
      @final_config.merge!(data) do |key, old_val, new_val|
        log_debug "Override: #{key} #{old_val.inspect} -> #{new_val.inspect}"
        new_val
      end
    end
    
    log_debug "Final config keys: #{@final_config.keys.join(', ')}"
  end
  
  def find_value_source(key)
    @config_sources.reverse_each do |source_name, data|
      return source_name if data.key?(key)
    end
    'unknown'
  end
  
  def key_info(key)
    {
      key: key,
      current_value: @final_config[key],
      source: find_value_source(key),
      all_sources: @config_sources.select { |_, data| data.key?(key) },
      access_count: @access_log.count { |entry| entry[:key].to_s == key }
    }
  end
  
  def full_debug_info
    {
      sources: @config_sources.keys,
      final_keys: @final_config.keys,
      total_accesses: @access_log.length,
      most_accessed: most_accessed_keys
    }
  end
  
  def most_accessed_keys
    @access_log
      .group_by { |entry| entry[:key] }
      .transform_values(&:length)
      .sort_by { |_, count| -count }
      .first(5)
      .to_h
  end
  
  def log_debug(message)
    puts "[CONFIG DEBUG] #{message}" if @debug
  end
end

# Usage for debugging configuration issues
config = DebuggableConfig.new(debug: true)

config.load_from_source('defaults', { 'host' => 'localhost', 'port' => 3000 })
config.load_from_source('environment', { 'host' => 'production.com' })
config.load_from_source('override', { 'timeout' => 30 })

host = config.get('host')  # Shows resolution process
puts config.debug_info('host')  # Shows detailed info for specific key
puts config.debug_info  # Shows overall configuration statistics

Production Patterns

Production configuration management requires patterns for secure storage, environment separation, configuration validation, monitoring, and deployment coordination. Ruby applications in production environments need robust configuration systems that handle secrets securely, support multiple deployment environments, and provide observability into configuration state.

Secret management separates sensitive configuration from regular settings, using encryption, external secret stores, and environment variable injection to protect credentials and API keys:

require 'base64'
require 'openssl'

class SecureConfig
  SecretNotFound = Class.new(StandardError)
  DecryptionError = Class.new(StandardError)
  
  def initialize(encryption_key: nil)
    @encryption_key = encryption_key || derive_key_from_env
    @secrets = {}
    @regular_config = {}
  end
  
  def load_encrypted_secrets(file_path)
    encrypted_data = File.read(file_path)
    decrypted_data = decrypt(encrypted_data)
    @secrets = YAML.load(decrypted_data)
  rescue Errno::ENOENT
    raise SecretNotFound, "Encrypted secrets file not found: #{file_path}"
  rescue OpenSSL::Cipher::CipherError
    raise DecryptionError, "Failed to decrypt secrets file - check encryption key"
  end
  
  def load_regular_config(file_path)
    @regular_config = YAML.load_file(file_path)
  end
  
  def secret(key)
    @secrets[key.to_s] || raise(SecretNotFound, "Secret not found: #{key}")
  end
  
  def config(key)
    @regular_config[key.to_s]
  end
  
  def database_url
    # Construct URL from separate secret components
    host = secret('database_host')
    user = secret('database_user')
    password = secret('database_password')
    database = config('database_name')
    
    "postgres://#{user}:#{password}@#{host}/#{database}"
  end
  
  def redis_url
    host = secret('redis_host')
    port = config('redis_port') || 6379
    password = secret('redis_password')
    
    "redis://:#{password}@#{host}:#{port}"
  end
  
  def self.encrypt_secrets(secrets_hash, encryption_key)
    cipher = OpenSSL::Cipher.new('AES-256-GCM')
    cipher.encrypt
    cipher.key = encryption_key
    iv = cipher.random_iv
    
    encrypted = cipher.update(secrets_hash.to_yaml)
    encrypted << cipher.final
    auth_tag = cipher.auth_tag
    
    # Combine IV + auth_tag + encrypted data and base64 encode
    combined = iv + auth_tag + encrypted
    Base64.strict_encode64(combined)
  end
  
  private
  
  def decrypt(encrypted_data)
    combined = Base64.strict_decode64(encrypted_data)
    
    # Extract IV (16 bytes), auth tag (16 bytes), and encrypted data
    iv = combined[0, 16]
    auth_tag = combined[16, 16]  
    encrypted = combined[32..-1]
    
    cipher = OpenSSL::Cipher.new('AES-256-GCM')
    cipher.decrypt
    cipher.key = @encryption_key
    cipher.iv = iv
    cipher.auth_tag = auth_tag
    
    decrypted = cipher.update(encrypted)
    decrypted << cipher.final
    decrypted
  end
  
  def derive_key_from_env
    key_material = ENV['CONFIG_ENCRYPTION_KEY']
    raise SecretNotFound, 'CONFIG_ENCRYPTION_KEY environment variable required' unless key_material
    
    # Derive consistent 32-byte key from environment variable
    OpenSSL::PBKDF2.pbkdf2_hmac(
      key_material,
      'ruby-config-salt',
      10000,
      32,
      OpenSSL::Digest::SHA256.new
    )
  end
end

# Usage in production
config = SecureConfig.new
config.load_regular_config('config/production.yml')
config.load_encrypted_secrets('config/secrets.yml.enc')

# Database connection with secrets
ActiveRecord::Base.establish_connection(config.database_url)

# Redis connection with secrets  
Redis.new(url: config.redis_url)

Configuration validation in production environments requires comprehensive checks at application startup, with clear error reporting for operators and deployment systems:

class ProductionConfigValidator
  ValidationError = Class.new(StandardError)
  
  def initialize
    @validations = []
    @warnings = []
    @errors = []
  end
  
  def validate_required_env(*keys)
    @validations << proc do
      missing = keys.select { |key| ENV[key].nil? || ENV[key].empty? }
      unless missing.empty?
        @errors << "Missing required environment variables: #{missing.join(', ')}"
      end
    end
  end
  
  def validate_database_connection(database_url)
    @validations << proc do
      begin
        # Simplified connection test
        uri = URI.parse(database_url)
        @errors << "Invalid database URL scheme: #{uri.scheme}" unless %w[postgres postgresql mysql2].include?(uri.scheme)
        @errors << "Database URL missing host" if uri.host.nil? || uri.host.empty?
        @errors << "Database URL missing database name" if uri.path.nil? || uri.path.length <= 1
      rescue URI::InvalidURIError
        @errors << "Invalid database URL format: #{database_url}"
      end
    end
  end
  
  def validate_port_range(port, name = 'port')
    @validations << proc do
      unless (1..65535).include?(port.to_i)
        @errors << "#{name.capitalize} must be between 1 and 65535, got: #{port}"
      end
    end
  end
  
  def validate_file_accessible(file_path, required: true)
    @validations << proc do
      unless File.readable?(file_path)
        message = "Cannot read file: #{file_path}"
        if required
          @errors << message
        else
          @warnings << message
        end
      end
    end
  end
  
  def validate_directory_writable(dir_path)
    @validations << proc do
      unless Dir.exist?(dir_path) && File.writable?(dir_path)
        @errors << "Directory not writable: #{dir_path}"
      end
    end
  end
  
  def validate_memory_limits
    @validations << proc do
      available_memory = `free -m | awk 'NR==2{print $7}'`.to_i rescue 0
      if available_memory > 0 && available_memory < 512
        @warnings << "Low available memory: #{available_memory}MB"
      end
    end
  end
  
  def run_validations
    @errors.clear
    @warnings.clear
    
    @validations.each(&:call)
    
    unless @warnings.empty?
      puts "Configuration warnings:"
      @warnings.each { |warning| puts "  - #{warning}" }
      puts
    end
    
    unless @errors.empty?
      error_message = "Configuration validation failed:\n" +
                     @errors.map { |error| "  - #{error}" }.join("\n")
      raise ValidationError, error_message
    end
    
    puts "Configuration validation passed"
  end
  
  def validation_report
    {
      passed: @errors.empty?,
      errors: @errors.dup,
      warnings: @warnings.dup,
      checks_run: @validations.length
    }
  end
end

# Production validation setup
validator = ProductionConfigValidator.new

validator.validate_required_env(
  'DATABASE_URL', 'REDIS_URL', 'SECRET_KEY_BASE', 
  'RAILS_ENV', 'LOG_LEVEL'
)

validator.validate_database_connection(ENV['DATABASE_URL'])
validator.validate_port_range(ENV['PORT'] || '3000', 'web server port')
validator.validate_file_accessible('config/application.yml')
validator.validate_directory_writable('log')
validator.validate_directory_writable('tmp')
validator.validate_memory_limits

begin
  validator.run_validations
rescue ProductionConfigValidator::ValidationError => e
  puts e.message
  exit 1
end

Configuration monitoring provides observability into configuration changes, access patterns, and runtime behavior. This pattern tracks configuration usage and exposes metrics for monitoring systems:

class MonitoredConfig
  def initialize
    @config = {}
    @access_count = Hash.new(0)
    @last_access = {}
    @change_log = []
    @start_time = Time.now
  end
  
  def load(source, data)
    timestamp = Time.now
    
    data.each do |key, value|
      old_value = @config[key]
      @config[key] = value
      
      if old_value != value
        @change_log << {
          key: key,
          old_value: old_value,
          new_value: value,
          source: source,
          timestamp: timestamp
        }
      end
    end
  end
  
  def get(key)
    @access_count[key] += 1
    @last_access[key] = Time.now
    @config[key]
  end
  
  def metrics
    uptime = Time.now - @start_time
    
    {
      uptime_seconds: uptime.to_i,
      total_keys: @config.keys.length,
      total_accesses: @access_count.values.sum,
      most_accessed: @access_count.sort_by { |_, count| -count }.first(5).to_h,
      recent_changes: @change_log.last(10),
      unused_keys: find_unused_keys,
      memory_usage: calculate_memory_usage
    }
  end
  
  def health_check
    {
      status: 'healthy',
      checks: {
        config_loaded: !@config.empty?,
        recent_access: @last_access.values.any? { |time| Time.now - time < 300 },
        no_recent_errors: @change_log.none? { |entry| entry[:error] }
      }
    }
  end
  
  def export_for_monitoring
    # Format suitable for Prometheus/monitoring systems
    {
      'config_keys_total' => @config.keys.length,
      'config_accesses_total' => @access_count.values.sum,
      'config_changes_total' => @change_log.length,
      'config_unused_keys' => find_unused_keys.length,
      'config_memory_bytes' => calculate_memory_usage
    }
  end
  
  private
  
  def find_unused_keys
    cutoff_time = Time.now - 3600  # 1 hour ago
    @config.keys.select do |key|
      @last_access[key].nil? || @last_access[key] < cutoff_time
    end
  end
  
  def calculate_memory_usage
    # Rough estimation of memory usage
    @config.to_s.bytesize + @change_log.to_s.bytesize
  end
end

# Integration with monitoring endpoint
class ConfigMonitoringEndpoint
  def initialize(monitored_config)
    @config = monitored_config
  end
  
  def metrics_endpoint
    # Returns Prometheus-style metrics
    metrics = @config.export_for_monitoring
    
    output = []
    metrics.each do |metric_name, value|
      output << "# TYPE #{metric_name} gauge"
      output << "#{metric_name} #{value}"
    end
    
    output.join("\n")
  end
  
  def health_endpoint
    @config.health_check.to_json
  end
  
  def debug_endpoint
    @config.metrics.to_json
  end
end

Common Pitfalls

Configuration management in Ruby contains several subtle issues that commonly cause production problems. These pitfalls involve constant modification attempts, class variable inheritance behavior, environment variable type assumptions, configuration loading timing, and security vulnerabilities from improper handling of sensitive data.

Constant modification represents a frequent mistake where developers attempt to change constants after definition. Ruby allows constant modification but issues warnings, leading to confusing behavior in applications:

# PROBLEMATIC: Attempting to modify constants
module BadConfig
  API_ENDPOINT = 'https://dev-api.example.com'
end

# Later in the application...
if Rails.env.production?
  # This generates a warning and causes confusion
  BadConfig::API_ENDPOINT = 'https://api.example.com'  # warning: already initialized constant
end

# BETTER: Use class variables or methods for runtime configuration
class ProperConfig
  @@api_endpoint = 'https://dev-api.example.com'
  
  def self.api_endpoint
    @@api_endpoint
  end
  
  def self.configure_for_environment(env)
    @@api_endpoint = case env
    when 'production'
      'https://api.example.com'
    when 'staging'  
      'https://staging-api.example.com'
    else
      'https://dev-api.example.com'
    end
  end
end

# BEST: Use environment-specific configuration from the start
class EnvironmentConfig
  ENDPOINTS = {
    'development' => 'https://dev-api.example.com',
    'staging' => 'https://staging-api.example.com',
    'production' => 'https://api.example.com'
  }.freeze
  
  def self.api_endpoint
    ENDPOINTS[Rails.env] || ENDPOINTS['development']
  end
end

Class variable inheritance creates unexpected behavior when subclasses modify class variables, affecting all classes in the inheritance hierarchy. This behavior often surprises developers expecting isolated configuration:

# PROBLEMATIC: Class variables shared across inheritance hierarchy
class BaseService
  @@timeout = 30
  @@retries = 3
  
  def self.configure(timeout: nil, retries: nil)
    @@timeout = timeout if timeout
    @@retries = retries if retries
  end
  
  def self.timeout
    @@timeout
  end
  
  def self.retries  
    @@retries
  end
end

class EmailService < BaseService
end

class PaymentService < BaseService  
end

# This configuration affects ALL services
EmailService.configure(timeout: 60)
puts BaseService.timeout      # => 60 (unexpected!)
puts PaymentService.timeout   # => 60 (unexpected!)

# BETTER: Use class instance variables for isolated configuration
class BetterBaseService
  @timeout = 30
  @retries = 3
  
  def self.configure(timeout: nil, retries: nil)
    @timeout = timeout if timeout
    @retries = retries if retries
  end
  
  def self.timeout
    @timeout
  end
  
  def self.retries
    @retries
  end
end

class BetterEmailService < BetterBaseService
  @timeout = 60  # Isolated configuration
  @retries = 5
end

class BetterPaymentService < BetterBaseService
  @timeout = 120  # Different isolated configuration
  @retries = 1
end

puts BetterEmailService.timeout     # => 60
puts BetterPaymentService.timeout   # => 120
puts BetterBaseService.timeout      # => 30 (unchanged)

Environment variable type conversion errors occur when developers assume environment variables contain non-string data or when conversion fails silently:

# PROBLEMATIC: Assuming environment variables are non-strings
class ProblematicConfig
  def self.port
    ENV['PORT'] || 3000  # Returns string "3000" when ENV['PORT'] is nil
  end
  
  def self.enabled?
    ENV['FEATURE_ENABLED']  # Returns string, not boolean
  end
  
  def self.worker_count
    ENV['WORKERS'] * 2  # String repetition, not multiplication
  end
end

# This causes subtle bugs
puts ProblematicConfig.port.class  # => String (should be Integer)
puts ProblematicConfig.enabled?    # => "false" (truthy in Ruby!)

# BETTER: Explicit type conversion with defaults
class ReliableConfig
  def self.port
    (ENV['PORT'] || '3000').to_i
  end
  
  def self.enabled?
    case ENV['FEATURE_ENABLED']&.downcase
    when 'true', '1', 'yes'
      true
    when 'false', '0', 'no', nil
      false
    else
      raise ArgumentError, "Invalid boolean value for FEATURE_ENABLED: #{ENV['FEATURE_ENABLED']}"
    end
  end
  
  def self.worker_count
    workers = ENV['WORKERS']
    return 2 unless workers  # Default value
    
    count = Integer(workers)
    raise ArgumentError, "Worker count must be positive, got: #{count}" if count <= 0
    count
  rescue ArgumentError => e
    raise ArgumentError, "Invalid worker count in WORKERS environment variable: #{e.message}"
  end
  
  def self.timeout_seconds
    timeout = ENV['TIMEOUT_SECONDS']
    return 30.0 unless timeout
    
    Float(timeout).tap do |value|
      raise ArgumentError, "Timeout must be positive, got: #{value}" if value <= 0
    end
  rescue ArgumentError => e
    raise ArgumentError, "Invalid timeout in TIMEOUT_SECONDS environment variable: #{e.message}"
  end
end

Configuration loading timing issues arise when configuration depends on other components that may not be initialized yet, or when configuration changes after components have already used the values:

# PROBLEMATIC: Configuration loaded before dependencies are ready
class ProblematicApp
  # This might fail if database isn't ready yet
  CONFIG = YAML.load_file('config/database.yml')
  
  def self.start
    database = Database.connect(CONFIG['database'])
    # Configuration already loaded, can't change based on runtime conditions
  end
end

# BETTER: Lazy configuration loading
class ReliableApp
  def self.config
    @config ||= load_configuration
  end
  
  def self.start
    # Configuration loaded when first accessed
    database = Database.connect(config['database'])
  end
  
  private
  
  def self.load_configuration
    config_file = determine_config_file
    YAML.load_file(config_file)
  rescue Errno::ENOENT => e
    puts "Configuration file not found: #{e.message}"
    puts "Using default configuration"
    default_configuration
  end
  
  def self.determine_config_file
    if File.exist?('config/local.yml')
      'config/local.yml'  # Development override
    elsif ENV['CONFIG_FILE']
      ENV['CONFIG_FILE']  # Explicit configuration
    else
      "config/#{ENV['RAILS_ENV'] || 'development'}.yml"
    end
  end
  
  def self.default_configuration
    {
      'database' => {
        'host' => 'localhost',
        'port' => 5432,
        'name' => 'app_development'
      }
    }
  end
end

# BEST: Configuration with initialization hooks
class ConfigurableApp  
  def self.configure
    yield(configuration) if block_given?
  end
  
  def self.configuration
    @configuration ||= Configuration.new
  end
  
  def self.start
    # Validate configuration before starting
    configuration.validate!
    database = Database.connect(configuration.database_options)
  end
  
  class Configuration
    attr_accessor :database_host, :database_port, :database_name
    
    def initialize
      @database_host = ENV['DATABASE_HOST'] || 'localhost'
      @database_port = (ENV['DATABASE_PORT'] || '5432').to_i
      @database_name = ENV['DATABASE_NAME'] || 'app_development'
    end
    
    def database_options
      {
        host: @database_host,
        port: @database_port,
        database: @database_name
      }
    end
    
    def validate!
      raise ArgumentError, "Database host cannot be empty" if @database_host.nil? || @database_host.empty?
      raise ArgumentError, "Database port must be positive" if @database_port <= 0
      raise ArgumentError, "Database name cannot be empty" if @database_name.nil? || @database_name.empty?
    end
  end
end

# Usage with proper initialization
ConfigurableApp.configure do |config|
  config.database_host = 'production-db.example.com'
  config.database_port = 5433
end

ConfigurableApp.start

Security vulnerabilities in configuration management occur when sensitive data appears in logs, version control, or error messages, or when configuration files have incorrect permissions:

# PROBLEMATIC: Exposing secrets in logs and errors
class InsecureConfig
  def initialize
    @database_password = ENV['DATABASE_PASSWORD']
    @api_key = ENV['API_KEY']
    puts "Loaded configuration: #{inspect}"  # Exposes secrets in logs
  end
  
  def inspect
    # This exposes sensitive data
    "#<InsecureConfig @database_password=#{@database_password} @api_key=#{@api_key}>"
  end
  
  def database_url
    "postgres://user:#{@database_password}@host/db"  # Password in connection string
  end
end

# BETTER: Secure handling of sensitive configuration
class SecureConfig
  SENSITIVE_KEYS = %w[password secret key token].freeze
  
  def initialize
    @config = load_configuration
    @sensitive_data = extract_sensitive_data
    log_configuration_loaded
  end
  
  def get(key)
    @config[key.to_s]
  end
  
  def secret(key)
    @sensitive_data[key.to_s] || raise("Secret not found: #{key}")
  end
  
  def inspect
    safe_config = @config.reject { |key, _| sensitive_key?(key) }
    sensitive_count = @sensitive_data.keys.length
    "#<SecureConfig #{safe_config.keys.length} config keys, #{sensitive_count} secrets>"
  end
  
  def database_url
    # Use connection options instead of URL to avoid password in logs
    {
      host: get('database_host'),
      port: get('database_port'),
      database: get('database_name'),
      username: get('database_username'),
      password: secret('database_password')
    }
  end
  
  private
  
  def load_configuration
    config_data = YAML.load_file('config/application.yml')
    
    # Validate file permissions
    file_stat = File.stat('config/application.yml')
    if file_stat.mode & 0o044 != 0  # World or group readable
      puts "Warning: Configuration file has overly permissive permissions"
    end
    
    config_data
  rescue Errno::ENOENT
    {}
  end
  
  def extract_sensitive_data
    sensitive = {}
    
    @config.each do |key, value|
      if sensitive_key?(key)
        sensitive[key] = value
        @config.delete(key)  # Remove from main config
      end
    end
    
    # Load additional secrets from environment
    ENV.each do |key, value|
      if sensitive_key?(key.downcase)
        sensitive[key.downcase] = value
      end
    end
    
    sensitive
  end
  
  def sensitive_key?(key)
    key_lower = key.to_s.downcase
    SENSITIVE_KEYS.any? { |sensitive| key_lower.include?(sensitive) }
  end
  
  def log_configuration_loaded
    safe_keys = @config.keys.reject { |key| sensitive_key?(key) }
    puts "Configuration loaded: #{safe_keys.length} settings, #{@sensitive_data.keys.length} secrets"
    puts "Configuration keys: #{safe_keys.join(', ')}" unless safe_keys.empty?
  end
end

Reference

Core Configuration Classes and Methods

Method/Class Parameters Returns Description
YAML.load_file(path) path (String) Object Loads YAML file and returns parsed data structure
YAML.load(string) string (String) Object Parses YAML string and returns Ruby objects
JSON.parse(string) string (String) Object Parses JSON string into Ruby objects
ENV[key] key (String) String, nil Retrieves environment variable value
ENV.fetch(key, default) key (String), default (Object) String, Object Retrieves environment variable with default
File.exist?(path) path (String) Boolean Checks if file exists at path
File.readable?(path) path (String) Boolean Checks if file is readable
Integer(value) value (Object) Integer Converts value to integer or raises ArgumentError
Float(value) value (Object) Float Converts value to float or raises ArgumentError

Environment Variable Conversion Patterns

Pattern Example Result Type Error Handling
String with default ENV['HOST'] || 'localhost' String Returns default if nil
Integer conversion (ENV['PORT'] || '3000').to_i Integer Returns 0 for invalid input
Safe integer Integer(ENV['PORT'] || '3000') Integer Raises ArgumentError for invalid
Boolean parsing ENV['DEBUG'] == 'true' Boolean Explicit string comparison
Array splitting ENV['HOSTS'].split(',') Array<String> Raises NoMethodError if nil
Safe array (ENV['HOSTS'] || '').split(',') Array<String> Returns [''] if nil

Configuration File Formats

Format Extension Loader Advantages Disadvantages
YAML .yml, .yaml YAML.load_file Human readable, comments, multi-line Slower parsing, security risks
JSON .json JSON.parse(File.read) Fast parsing, widely supported No comments, no multi-line strings
Ruby .rb require_relative Full Ruby syntax, conditional logic Security risk, harder validation
INI .ini Custom parser Simple format Limited nesting, no standard parser

Common Configuration Errors

Error Class Typical Cause Prevention
Errno::ENOENT Missing configuration file Check file existence, provide defaults
Psych::SyntaxError Invalid YAML syntax Validate YAML, use linting tools
JSON::ParserError Invalid JSON syntax Validate JSON, handle parse errors
ArgumentError Invalid type conversion Validate input before conversion
NoMethodError Calling method on nil Check for nil before method calls
KeyError Missing required key Validate required keys at startup

Security Best Practices

Practice Implementation Benefit
File permissions chmod 600 config/secrets.yml Prevents unauthorized access
Environment variables Store secrets in ENV, not files Keeps secrets out of version control
Encryption at rest Encrypt configuration files Protects secrets in backups
Secret rotation Regular key/password updates Limits exposure window
Audit logging Log configuration access Enables security monitoring
Validation Check configuration at startup Fails fast on misconfiguration

Configuration Loading Order

Priority Source Example Use Case
1 (Highest) Command line arguments --port=8080 Runtime overrides
2 Environment variables PORT=8080 Deployment configuration
3 Local config files config/local.yml Development overrides
4 Environment config config/production.yml Environment-specific settings
5 Base config config/base.yml Default values
6 (Lowest) Application defaults Hard-coded constants Fallback values

Configuration Validation Patterns

Validation Type Code Pattern Error Type
Required key raise "Missing: #{key}" unless config[key] StandardError
Type checking raise "Invalid type" unless value.is_a?(Integer) TypeError
Range validation raise "Out of range" unless (1..100).include?(value) RangeError
Format validation raise "Invalid format" unless value =~ /\A\w+\z/ ArgumentError
Dependency check raise "Requires X" if needs_x && !config[:x] ConfigurationError
Custom validation validate_custom(value) or raise "Invalid" Custom error

Thread Safety Considerations

Configuration Type Thread Safety Synchronization Needed
Constants Thread-safe No (immutable)
Class variables Not thread-safe Yes (mutex)
Class instance variables Not thread-safe Yes (mutex)
Instance variables Thread-safe per instance No (if not shared)
Environment variables Thread-safe No (read-only)
File-based config Not thread-safe Yes (for writes)

Performance Characteristics

Configuration Source Load Time Memory Usage Reload Cost
Constants Negligible Low Cannot reload
Class variables Negligible Low Low
Environment variables Negligible Very low Low
YAML files Medium Medium Medium
JSON files Low Low Low
Ruby files High Variable High
Database High Low High
Remote APIs Very high Low Very high