Feature #21194
openHow to manage application-level information in Ruby application
Description
Goal¶
I want to manage application-level information (e.g., application configuration) while making it easily accessible from the part classes of the application. Additionally, I want to support multiple instances of the application within a single process.
Current approach 1: Global variables¶
The simplest way to achieve this is by using global variables.
class MyApp
  class Part1
    def run
      using $config[:part1]...
    end
  end
  class Part2
    def run
      using $config[:part2]...
    end
  end
  def initialize(config)
    $config = config
    @part1 = Part1.new
    @part2 = Part2.new
  end
  def run
    @part1.run
    @part2.run
  end
end
app1 = MyApp.new({ part1: "aaa", part2: "bbb" })
# app2 = MyApp.new({ part1: "AAA", part2: "BBB" }) # Cannot create this
app1.run
This code is simple and clear, but it does not allow creating multiple MyApp instances with different configurations.
To achieve that, we would need to create separate process using fork or spawn.
This limitation remains even if we replace global variables with constants (MyApp::Config) or class methods (MyApp.config).
Current approach 2: Passing configuration via initialize
¶
A textbook and well-structured approach is to explicitly pass configuration through initialize.
class MyApp
  class Part1
    def initialize(config)
      @config = config
    end
    def run
      using @config[:part1]...
    end
  end
  class Part2
    def initialize(config)
      @config = config
    end
    def run
      using @config[:part2]...
    end
  end
  def initialize(config)
    @part1 = Part1.new(config)
    @part2 = Part2.new(config)
  end
  def run
    @part1.run
    @part2.run
  end
end
app1 = MyApp.new({ part1: ..., part2: ... })
app2 = MyApp.new({ part1: ..., part2: ... })
app1.run
app2.run
This approach allows creating multiple MyApp instances with different configurations in a single Ruby process.
However, it has two major drawbacks:
- 
configmust be passed explicitly in everyinitializeandnewcall, making the code verbose. - Both 
Part1andPart2instances hold their own@configvariables, which is redundant -- especially when creating a large number of small instances (e.g., tree nodes). 
Current approach 3: Thread-local storage¶
Storing configuration in Thread[:config] allows multiple application instances without explicit parameter passing.
class MyApp
  class Part1
    def run
      using Thread[:config][:part1]...
    end
  end
  class Part2
    def run
      using Thread[:config][:part2]...
    end
  end
  def initialize(config)
    @config = config
    @part1 = Part1.new
    @part2 = Part2.new
  end
  def run
    Thread[:config] = config
    @part1.run
    @part2.run
  end
end
app1 = MyApp.new({ part1: ..., part2: ... })
app2 = MyApp.new({ part1: ..., part2: ... })
app1.run
app2.run
This approach is mostly effective but has an issue:
- 
Thread[:config] = @configmust be set at the beginning ofMyApp#run. While this is manageable if there is only one public API, it becomes error-prone when multiple APIs exist. 
Note that using Fiber#[] instead of Thread#[] has the same issue.
Proposal¶
Ideally, we want to support multiple application instances while keeping the simplicity of the global variable approach.
To achieve this, I propose introducing a new type variable, such as $@config:
- 
$@configbelongs to an instance - When accessing 
$@config, it is looked up not only inselfbut also by traversing the call stack to find the nearestselfinstance that has$@config. 
With this, the code could be written as follows:
class MyApp
  class Part1
    def run
      $@config[:part1] # accesses MyApp's $@config
    end
  end
  class Part2
    def run
      $@config[:part2]
    end
  end
  def initialize(config)
    $@config = config
    @part1 = Part1.new
    @part2 = Part2.new
  end
  def run
    @part1.run
    @part2.run
  end
end
app1 = MyApp.new({ part1: ..., part2: ... })
app2 = MyApp.new({ part1: ..., part2: ... })
app1.run
app2.run
This behaves similarly to dynamically scoped variables but differs in that it is resolved through the self instances.
(Thread.new is a bit problematic: if you use Thread.new in a method of MyApp::Part1, you wouldn't have access to $@config in it. It might be nice to take over all $@x variables.)
Feedback wanted¶
Whenever I write a large Ruby application, I encounter this problem.
However, TBH, I am not entirely confident that my proposed solution is the best one.
Do you ever encounter this problem? How do you deal with the problem when you do? Is there a better workaround?
No data to display