Configuration and Dependency Acquisition

I’ve written some prior posts on the strange state of affairs that got those of us in Java-land to MicroProfile Config and the many ways in which I find it bizarre, unpredictable, clumsy and deficient. In this post I’d like to talk about what configuration systems actually depend on so we can locate them in architectural space before proceeding further.

I’ve indirectly made the case that a Java configuration system is fundamentally in the business of loading objects and handing them to you. That’s it. You ask for an object; it is delivered unto you. It is not fundamentally about conversion, “properties” and all the usual stuff people tend to bring up when they talk about Java configuration because they are subconsciously thinking about System properties and environment variables, both of which are different things and are the way they are due to requirements that have nothing to do with Java configuration per se. (But I digress.)

Next little factoid: Most Java applications these days either knowingly use a dependency injection framework (Spring, CDI) or unknowingly invent a bad version of one. Dependency injection frameworks load and make objects and hand them to you. Hmm. That sounds familiar.

Suppose now you want to use a configuration system to configure, among possibly other things, the dependency injection framework you’re using. If configuration is all about loading objects and handing them to you, and, arguably, dependency injection is at least partially about the same thing, then how do the dependencies between these two architectural nuggets work?

Cutting to the chase, the DI framework has to depend on the configuration framework, or the configuration framework couldn’t configure it before it started. So no dependency injection for you while you’re in the configuration system. That much is obvious, and yet there are still configuration systems of all sorts of stripes that get this wrong.

What may be less obvious at first glance is how to leave object production as much as possible in the hands of the dependency injection framework, and configuration as much as possible in the hands of the configuration framework. In other words, in CDI I can produce a String, and in a configuration system I can look up a String. How are these operations different? Which should I use at which points?

Or to put it another way, for a well-designed configuration system to do its job, it does in fact need to be a very limited version of a dependency acquisition (if not injection) framework itself. Fortunately, the barrier to entry is much lower for configuration systems than full-blown dependency injection systems. For example, configuration systems only really have to worry about getting The Things That Make Configured Objects Of Particular Kinds, not any object in the world. This cuts down dramatically on scope and lifecycle requirements and proxying. Also, since configuration systems are bootstrappy by their very nature, they don’t really need injection or circular reference handling or any of the other things that justifiably make dependency injection systems inherently complicated.

One great absolutely minimal example of this sort of thing that is deliberately extremely limited is java.util.ServiceLoader. This “configuration framework” instantiates an assignable class given a superinterface (or superclass) that often you have defined, and hands it to you. That’s it. That’s still a dependency acquisition framework: wherever you are, whatever architectural layer you inhabit, you ask the ServiceLoader for a provider object, and then manipulate that provider object to get you your dependency. One of the many things that’s nice about this is you can supply the types involved, so the configuration system stays out of this business. The configuration system also tells you exactly how it’s going to make a match between what you asked for and what’s available. None of this involves type conversion, scalar munging, namespaces, weak typing or other frippery. Said frippery may of course reside “lower down” the architectural stack, but that’s not the business of the ServiceLoader nor should it be.

Suppose ServiceLoader were enhanced a little bit (let’s pretend) to be able to sort through multiple service providers to hand you one that was the most suitable for you in some fashion (right now, ServiceLoader just reports all the providers it found). That would be a great example of a helpful configuration system: you ask it for a Car, and it sorts through all the various Car providers to find the one that is suited to where your application happens to be located in configuration space and picks that one. Then it tells the selected provider to get you your Car. Is it a perfect match? Maybe, but maybe not. But it is a suitable match. Will the provider supply the same Car every time it’s asked for one? Maybe, but maybe not. If the application wants a stable Car, then it caches the Car. If it does not, then it asks the provider for a Car again. This is not the business of the enhanced hypothetical ServiceLoader either.

(As an aside: MicroProfile Config dimly glimpses that this is the way to go (a Config finds a suitable ConfigSource, but falls down by then forcing all such service providers to be in the business of producing strings that other service providers (Converters) convert into what you need. Now because you are producing things of a type the application developer didn’t hand you, you have to worry about names, weak typing and namespaces when that’s not anything at all that the application developer wants to work with, and that’s how you get such a strange API.)

Anyway, armed with such a thing, then you can cause those configured objects to surface in your dependency injection framework however you like. If, for example, you’re in the middle of writing a CDI portable extension (so something executing before the dependency injection system has completed initializing), you could use the configuration system to acquire objects you need to figure out how your extension should behave, and then if they’re relevant elsewhere in the application, you could write producer methods for them, or the provider that makes them, and now the rest of your ordinary CDI code could just use them as any other bean. If you carry the dependency injection mindset to its logical conclusion, an application really shouldn’t, itself, need to consult some special system for configuration data. It will just use its normal objects, some of which will have been configured and produced by DI system internals.

If you think of things this way, then you can start to see what shape a configuration system in Java should take. More (obviously) on this topic to come.

Author: Laird Nelson

Devoted husband and father; working on Helidon at the intersection of Java, Jakarta EE, architecture, Kubernetes and microservices at Oracle; open source guy; Hammond B3 player and Bainbridge Islander.

%d bloggers like this: