Re-Organized configuration in Rails
A while back I wrote about organizing configuration in Rails . The idea was simple: drop YAML files into config/configurations/ and get namespaced constants like Config::Bot.api_key instead of the clunky Rails.application.config.bot.api_key . It worked well. But every YAML file needed manual wiring: <%= ENV.fetch("BOT_API_KEY", Rails.application.credentials.dig(:bot, :api_key)) %> . For every key. Across every file. Ugh! So I rebuilt it. Same clean Config::Namespace.key API, but now it chains through all three sources automatically. Before (old module’s YAML): # config/bot.yml shared : api_key : <%= ENV.fetch("BOT_API_KEY", Rails.application.credentials.dig(:bot, :api_key)) %> user_agent : " MyAwesomeBot/1.0" timeout : 10 After (new module’s YAML): # config/bot.yml shared : # Config::Bot.api_key is still available and will check environment variables and then check credentials user_agent : " MyAwesomeBot/1.0" timeout : 10 One API. Three sources. No more guessing where a value lives. You can find the full code on GitHub . What follows are the parts I find most interesting. Lazy namespaces with const_missing The old version scanned a directory at boot and called const_set for every YAML file. That works, but it means every namespace is loaded whether you use it or not. This version uses const_missing instead. Reference Config::Bot for the first time and a Namespace object is created lazily: def self . const_missing ( name ) MUTEX . synchronize do @namespaces ||= {} @namespaces [ name ] ||= Namespace . new ( name ) end end The Mutex isn’t there by accident. In threaded environments (Puma, Solid Queue), two threads could hit const_missing simultaneously. Mutex makes sure only one namespace object gets created. The three source chain Each Namespace uses method_missing to resolve a key: def method_missing ( method , ... ) key = method . to_s . delete_suffix ( "!" ) bang = method . to_s . end_with? ( "!" ) environment_key = " #{ @prefix } _ #{ key . upcase } " return @envi