Have you ever been working on a Rails application, needed to use a configuration value within your code and then couldn't remember whether it's a setting, secret, credential, or environment variable? If you haven't, this won't be interesting, but if you're like me, it might be worth reading on.
I’ve also encountered a variety of other solutions that attempt to address some of these challenges. Personally, I’ve been hoping for a built-in way to do this in Rails without requiring gems for such a core and critical piece of functionality, but many of these look to be really good.
With all of that said, I certainly don’t think there’s one perfect answer for this, but there’s a lot of existing evidence that we collectively feel there’s room for improvement. With all that inspiration, surely there’s some potential for a built-in solution.
Rails provides great support for initializing configuration, secrets, credentials and environment variables, but I’ve always struggled a little with accessing the values. Not a deep struggle but some frequent friction on the differences between how credentials and configuration are exposed. Using ENV has always felt a little inelegant in some contexts as well.
Onto the idea…
As it stands now, looking at these side-by-side, despite similarity in use cases, there’s some consistency mixed in with just enough variation to make it easy to trip up. (Figure 1)
While that’s not inherently a bad thing, it created an itch I wanted to scratch.
Ultimately, each of the approaches for accessing the values are simultaneously incredibly similar and just different enough to get wires crossed. So I thought it might be handy to have source-indifferent lookups that offered a more consistent interface for retrieving the values without having to think as much about nuances of where they were stored or how they’re supposed to be read. (Figure 2)
We’ll look at the code first, and then we’ll dive into the pros and cons that I’ve found based on some initial exploration and trying out this approach on a production Rails app.
That provides the broad strokes of the idea, but I’ve got some working (and tested) code that shows more of the usage and versatility at the end of the post.
The benefits I’ve found from this approach…
The interface for retrieving the value is consistent regardless of the source of the value. I spend less time thinking about syntax, return values, and defaults.
Accessing the values focuses more on how they’ll be used in context rather than where or how the value is stored. In some cases, for instance, if a value is missing, the application can still run just fine. In other cases, if a value is missing (due to environment differences, typos, or whatever), it’s best to stop right away and draw attention to the fact that the value is missing. Otherwise, a nil configuration setting can be passed around in the code and not fail until it’s much less clear where the problem originated.
It’s a little more intention-revealing about the expectation for how the value will be used. Accessing a value via a required or optional method immediately conveys the significance of the value’s presence.
With defaults, it provides a consistent approach regardless of the return value for the setting. There’s no need to think about which approach for providing a default return value is appropriate. i.e. || or fetch(:key, 'Default') or fetch(:key) { 'Default' }.
In cases where production/staging rely on Environment Variables like Heroku’s Config Vars, the retrieval can work independently so that test/development can explicitly ignore the value or find it via configuration instead.
It provides a home for some richer exceptions to help catch configuration issues earlier and reduce debugging second-order effects of configuration value issues.
If all else fails the existing ways of accessing values would still be available.
The caveats/room for improvement…
Performance. It is currently slower due to the extra level of indirection. In real-world usage for these kinds of values, it seems likely to be negligible relative to the benefits it provides. (Benchmarks at the end.) Or an integrated Rails solution could potentially address that.
Abstracting the source from lookups could be problematic. Sometimes, the friction is a good thing and explicitly implementing the lookup logic in the context it’s used can be helpful. While this wouldn’t be prevented, it becomes less obvious that it’s an option to direclty access the values. Maybe it could be extended to help reduce ambiguity between the various simultaneous approaches to secrets/credentials? (i.e. Settings.credentials., Settings.configuration., and Settings.env.)
If this existed in parallel to the existing options rather than replacing them, it could create more ambiguity and confusion about which method is the best method. But at the same time, the fact that this does not change any of the existing API’s means it doesn’t directly interfere.
As it stands, it’s far from something that could be dropped right into Rails core, but it seemed worth discussing about whether a built-in Rails equivalent could be worth exploring further. The existing approach was more of a way to try it out in practice to see if it felt better. i.e. What is currently the Settings class would likey need to live off of Rails rather than be a standalone class among other things.
It does not currently account for the Rails.configuration.x.<method> approach–only the Rails.configuration.<method>. I don’t believe that would be difficult to add support for, though.
There are some additional access methods via method_missing and source-specific options as well. They’re documented in the source code. (Below with benchmarks.) I can’t say I have strong feelings yet about whether all of the approaches are necessary or their relative usefulness.
It is possible to have ‘conflicts’ where there’s a key or set of keys that exist in more than one source. Right now, when this happens, it raises an exception to draw attention to the problem. Then the conflicting key can be renamed, deleted in which ever file it shouldn’t be in, or accessed via the source-specific options. i.e. Settings.secret(:key), Settings.config(:key), or Settings.env(:key).
This implementation behaves as a wrapper, but a better approach for Rails may be to expose more consistent access methods to each of the sources rather than route access through an entirely new layer.
It doesn’t currently account for scenarios where the optional/required/default approach could differ by environment. That’s likely not a bad thing, but it could be an opportunity to make it a little more elegant. Or it could complicate matters and make the resulting interface more confusing.
Loading/initialization order means the Settings class isn’t currently accessible early in the boot process, but I believe that’s a solvable problem, especially if this was more natively integrated with Rails.
Learn to Build Custom Generators that Save Time
Skip the copy/paste and search/replace dance. Learn how custom Rails generators can help save time for you and your team. I’ve put together a free email course to help demystify things so you can create custom generators quickly and efficiently.
While my specific implementation leaves plenty to be desired, the interface and benefits feel worth pursuing/exploring in the larger context of Rails. And to share the performance aspects, here’s the benchmark I used and the results (Figures 3 & 4)
. So far, I haven’t given any thought to improving performance, but I did want to at least consider how much of a problem it could be. So the performance gap could be very easy to close.
The bencharmks illustrate that there is some overhead with this approach, but as infrequently as these kinds of values are loaded and accessed, it may not be a significant issue at all. More likely, I’m fairly confident I could improve the performance if the API is interesting enough to explore further.
And the actual code and tests in case anybody else wants to take it for a spin. it includes some additional methods and options, but it’s still more of a working prototype than a definitive proposal.