Some of the Things I Don’t Like About MicroProfile Config

Here are some things I don’t like about the MicroProfile Config APIs (and really also most APIs like them) and the ways in which they are defined (or not defined) to work. I’m writing them down in no particular order so I don’t forget. This is obviously related to my prior post.

If I’m handed an opaque application that uses MicroProfile Config, I don’t know the names or types of the configuration properties it will seek, if any. So I don’t know what configuration to author.

If your opaque library happens to seek “fred” (and it’s opaque so I can’t know this up front; see above; but let’s say I use strings or something and get a good guess) and my library also seeks “fred“, and yours expects a certain String format for “fred” (so that it can convert it to some specialized object), and mine expects a different String format for “fred“, and I combine the two libraries into an application, and there are Converters from you and Converters from me and various ConfigSources involved, there’s no way that I can see to reconcile this other than trial and error at runtime.

If I put a ConfigSource on the classpath with ordinal Integer.MAX_VALUE that returns a random String for every value sought, or do something well-intentioned but accidental like this, pretty much everything will fail at runtime and it’s difficult to understand why. (In general, if you have to resort to ordinals or priorities in anything, you have already lost; what you’re actually after is a reduction algorithm.)

A ConfigSource can report whatever it wants from getOrdinal, so even if you set a config_ordinal property in its backing data somehow, you don’t know if your setting will be used.

A ConfigSource can return whatever it likes as its name, and since this is used as a tiebreaker in sorting, sorting is indeterminate. So which ConfigSource “answers the phone” is also indeterminate from run to run.

We insist that developers model business objects because developers know the names and types of the data they use (Person has a getAgeInYears() method that returns a positive int, not a method like getProperty(String name, Class<?> type)). But MicroProfile Config forces configuration (where the application also knows the names and types of the configuration data it consumes) to be consumed via dynamically-typed, Map-like probe requests (Config.getValue("personAge", int.class)). That’s very odd.

A Config can return an empty set for the configuration property names it claims to support because the set of such available names may change from moment to moment, or may not be able to be determined for any reason. So an application developer doesn’t know what she can ask for via the probing APIs. (Can she ask for “fred“? If she asks for “fred” twice and gets two null responses, does that mean that “fred” is never supported? Or maybe just that coincidentally “fred” couldn’t be retrieved during that particular time window? She doesn’t know, and can’t know.)

A ConfigSource can do whatever it wants, as can a Converter, as can ConfigSourceProviders, and they can all come from the classpath, whether user-, application-, application-server- or library-supplied, or all four, and a Config is essentially just a shell around these ConfigSources and Converters and ConfigSourceProviders, so not only do you not know what you can ask for, you don’t know whether it will be delivered to you for any given call you make, or, if it can, whether it can be delivered to you with the type you want, or, if you make it this far, whether such typed data can be delivered to you again with the same type if you make the same call, or if you restart the application if the same things you observed last time will hold true this time.

Since a Converter can do whatever it wants and can be user- or system-supplied, then even with the Config::getConverter method you don’t know what kind of suitable conversion is available at any given point for any given property.

Given a configuration property named “fred“, there’s no way to know what type(s) its value may successfully be converted to (other than String).

Configuration, in other words, is by spec unpredictable in name, type, availability, idempotency and determinism. That’s bizarre.

While the APIs and specified behavior for the Config-locating-and-assembly subsystem (ConfigProviderResolver) to conceivably work with any ConfigSource and Converter and Config implementations, in practice doing things like mixing and matching one vendor’s ConfigProviderResolver with another’s ConfigProvider doesn’t work.

Applications are associated with ClassLoader instances in some vague way. They could be associated with Object instead (provided the Object-in-question’s equals and hashCode methods are stable) and everything would still work. This tells you that ClassLoader association is unnecessary.

When a classloader is unloaded, various internal resources inside any MicroProfile Config implementation must be released, but there’s no real specification about how this should be done. In practice, to do this properly

There’s no specified way to configure MicroProfile Config with MicroProfile Config. There are occasional places where the need for this is visible and an attempt has been made to address it (config_ordinal, ConfigSourceProvider), but with no real systemic approach.

Notions of how to close and otherwise release resources including Converters inside any MicroProfile Config implementation require absurdly complicated WeakReference machinations in any specification-compliant implementation for no really good reason (see above). Otherwise you leak ClassLoaders.

Since you don’t know from call to call whether “fred” is available, let alone what type it can be converted to (since its format could change or conversion could be random), then injection of its value via CDI should really be defined in terms of only Provider injection. Alas. So an Integer-typed, @ConfigProperty-annotated CDI injection point that was “validated early” can fail later, or not, at runtime, which is the very sort of thing CDI is designed to prevent.

Overall, the APIs conflate the problems of object acquisition (“get me a ham sandwich”) with those of object production (“make a ham sandwich out of its parts and hand it over”), binding (“to make a ham sandwich, find bread, ham, cheese, mayo, mustard and lettuce”) and sourcing (“you find the bread in the breadbox unless it’s already out and then it’s on the counter, the cheese is in the cheese drawer, and the mayo is usually on the top shelf of the fridge, unless you have to open a new bottle….”). Then the results are served up with unpredictable scalar probing methods (unless I’m using CDI injection, I have to ask for the bread, then the condiments, then the lettuce and put them together, assuming I actually got results for these). This is all very strange. I just want a ham sandwich. The person making the ham sandwich probably cares a great deal about all this stuff, but I wouldn’t be asking them for a ham sandwich if I wanted to make it myself! (Also I’m never going to convert bread to mustard.)

Finally, an application developer also basically just wants a ham sandwich. She wants to receive a HerApplicationConfigThatSheDesigned object in some ceremony-free manner that’s appropriate for the environment her application is in, and call typed methods on it at various points during the lifespan of her application to get the information she needs, handling errors in the way that she sees fit. You can see examples of this in the wild. Consider this, and then re-read what’s above and sit with it.

There are so, so, so many ways this whole area could be better. I hope to say more soon.

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.