A Pattern Some Would Call an Anti-Pattern
In a previous piece on tracing request IDs, I mentioned a pattern called the request store that makes a request ID conveniently available from any point in an application. The idea is simple: store data into Ruby's thread-local context:
# request store that keys a hash to the current thread
module RequestStore
def self.store
Thread.current[:request_store] ||= {}
end
end
Middleware then ensures that all context added to the store is cleared between requests:
class Middleware::RequestStore
...
def call(env)
::RequestStore.store.clear
@app.call(env)
end
end
In larger applications, I tend to extend this pattern by explicitly inventorying what the store is supposed to contain. This makes it harder to accidentally create opaque dependencies by throwing in unrelated data:
module RequestStore
def log_context ; store[:log_context] ; end
def request_id ; store[:request_id] ; end
def log_context=(val) ; store[:log_context] = val ; end
def request_id =(val) ; store[:request_id] = val ; end
private
def self.store
Thread.current[:request_store] ||= {}
end
end
Why It Looks Wrong on Paper
The request store, much like the singleton pattern, introduces global state into the application. That makes it harder to reason about what any given piece of code depends on. Global state also tends to complicate testing: implicitly initialized globals can be difficult to set without a stubbing framework, and they keep their value across test cases, which can surprise anyone who does not expect it.
This approach is less controversial in dynamic languages, where something like a Global Interpreter Lock (GIL) protects against cross-thread race conditions. Still, colleagues from more pattern-driven enterprise environments would likely frown on using global state of any kind. They would instead reach for a dependency injection framework to make certain information available application-wide.
Why It Works Anyway
From an engineering standpoint, the side effects of using the request store over time have been minimal here. As long as we stay vigilant—ensuring it does not creep beyond its originally intended use—it becomes a convenient place for a few pieces of global state that would otherwise be awkward to access. We keep it in check through pull request discussions and consensus on what may be added.
The request store is not an isolated case. Projects like Rails and Sinatra have long relied on global patterns for things like managing database connections and delegating DSL methods from the main module. These uses may have caused grief for some over the years, but their longevity is a testament to their practical success.
Anti-patterns that keep showing positive productivity results and cause minimal harm are worth keeping around.



