🔔 This article was originally posted on my site, MihaiBojin.com. 🔔
For the past year, I've been on and off thinking of a common problem: handling properties in Java. While the problem seems simple, dealing with multiple sources, optionality, secrets, etc., is not trivial.
I started by looking for existing implementations, but I couldn't find many.
The closest I found was Netflix's Archaius, but it didn't do everything I needed and had features I didn't need. However, what sealed the deal was that the library seemed abandoned, with the last minor release having happened in 2019.
I decided to build my own (#NIH, I know 😝).
My first attempt resulted in the props library. For this, I had a few goals.
Since these sources are text-based, the values are first loaded as strings. However, strings can be deserialized into many types, which is what the application would expect. Hence, we want such a library to type cast values into the appropriate (standard or user-defined) types. We could do that every time a value is read. However, doing so on every read would result in many unnecessary type conversions. A better approach is to cast once and memoize the typed value until and if it changes.
Apart from its value, a prop has other attributes (metadata). The metadata includes:
- if a prop is required or optional
- if it has a default value
- if it represents a secret (in which case extra care needs to be taken)
- a description of what the property is used for, aiding with documentation
A key could be defined in more than one source, so the library needs to determine which source owns which key.
Then, the values could change at the source; to tackle that, we'd need a mechanism to check for updates and refresh any updated keys periodically.
We also want to account for extendability. For example, a client of this library might need to define new sources (e.g., read properties from a MongoDB database or an HTTP endpoint). To that end, we provide an interface that users can implement to add extra functionality.
Another use case is managing secrets. While it is best to use production-ready solutions (e.g., a KMIP-based server), some applications require keys to be defined in property files. In that case, it is best to encrypt them at rest. Encrypting generates binary output; a common way to represent binary data as text (e.g., in a property file) is to base64-encode it. Our library would need to be able to deserialize a base64-encoded, encrypted value as a string seamlessly.
Since I wanted to use this library in Java projects, I set up a CD pipeline to release it to Maven central.
I have built Props v1 with the above in mind.
However, I was too focused on building a proof of concept and didn't think too much about performance. I also built this first version with a static use-case in mind (the client is responsible for refreshing a prop's value.)
There are, however, cases where a client would also benefit from being notified when values change. Upon realizing that, I added support for notifying subscribers of updates. I also started looking at where the bottlenecks were. While the library is by no means slow, a few original decisions make it difficult to improve.
This is a common problem in software engineering. For established libraries with significant user adoption, it is costly to rearchitect from scratch. Usually, the code would be updated slowly, taking great care to avoid breaking changes.
Since this is a personal project with no current production usage - I have a bit of flexibility.
I have decided to rewrite the code with performance in mind. However, I also want to focus on asynchronous primitives, which are a better choice for today's software.
Imagine the following scenarios:
A. A property is loaded once from a configuration file and used to configure the maximum number of requests allowed by the application in a given interval. When the application needs to be reconfigured, the file is updated, and the application restarted. Restarting is not instantaneous, and it can result in downtime for users. To avoid it, multiple instances need to be run and managed by an external orchestration service (e.g., Kubernetes).
B. The same property is aware of its origin and can notify the implementing code of any updates. The application can then reconfigure itself without fully restarting.
There is, of course, no 100% right solution. If a platform already manages the application, and multiple instances are deployed and running, and if restarting the service is a fast enough endeavor, Solution A may be perfectly viable. In most cases, however, at least having the option of using Solution B is a positive net benefit: it's there, but you don't have to use it. Not having it, though, could be limiting. This logic could be extended to replacing secrets, enabling a new feature for an increasing subset of users, etc.
Let's define a few common terms:
- source : where a key=value pair originates from
- prop : an object that can return a type cast value for its registered key
- registry : an object that keeps track of all sources and props
- layers : a mechanism that helps to establish the priority in which sources own a particular key
- effective value : the final value of a key, taken from the source that owns it
Let's outline a few design considerations.
Sources must be layered in the sense that they should establish clear precedence over which source ultimately owns which key.
When a value is updated on a source, and that source owns the prop, the prop needs to be (eventually) updated with the new value. "Eventually" means that a prop may not see all the state transitions (writes) between reads. It's important to note that I am not designing an event streaming solution. Therefore, some updates may never reach the client.
We need to choose a 1:1 or 1:many mapping between keys and prop objects in the registry. On the one hand, allowing more than one prop for any given key could be flexible. For example:
var prop1 = new StringProp("my.key"); var prop2 = new StringProp("my.key");
Multiple callers could bind and use new prop objects without any limitations. However, I believe this choice would enable users to write inefficient code.
The next question is, should we allow props of different types to be bound to the same key?
Consider the following code:
var prop1 = new DurationProp("my.key"); var prop2 = new LongProp("my.key"); var prop3 = new StringProp("my.key");
You could argue that all three forms are needed, but the same could be achieved in the following way:
var prop = new DurationProp("my.key"); Duration result = prop.get(); Long seconds = result.toSeconds(); String stringValue = result.toString();
Asynchronous updates would need to be propagated fast. There is no point in implementing such a library only to support a few updates per second or only a few tens of keys defined in the system.
Let's focus on performance by having the library restrict one prop object per key to keep the number of prop objects small and the implementation lean.
Since sources can specify custom update intervals, we can assume values could change, and keys could appear or disappear , on multiple sources, in quick succession.
Let's look at a few scenarios for a given key defined on two sources:
|Prop is defined by source 1||Source 1, val=1||1|
|Prop is defined by source 2||Source 2, val=2||2|
|Prop is updated by source 1||Source 2, val=3||2|
|Prop is updated by source 2||Source 2, val=4||4|
|Prop is deleted from source 1||Source 2||3|
|Prop is defined by source 1||Source 2, val=5||4|
|Prop is deleted from source 2||Source 1||5|
|Prop is defined by source 2||Source 2, val=6||6|
|Prop is deleted from source 2||Source 1||5|
|Prop is deleted from source 1||N/A||null|
Our implementation needs to handle all cases correctly. These could happen in fast succession for a large number of keys, so it's essential to handle these state transitions as efficiently as possible.
Some of them are easier than others; for example, updating a key's value without changing source ownership is straightforward. However, changing the source that owns a key is more involved since we need first to determine the correct source that should provide the effective value.
There is a real possibility of update storms ; for example, a source could repeatedly own and disown a key over a short interval. This is probably more of an edge case than a legit need. In any case, the implementation should be able to:
- effectively deal with ownership transitions in quick succession
- flag when this behavior occurs so that a human operator can investigate the correctness
It's important to note that this case is different than everchanging values (e.g., a boolean flag that keeps flipping from true to false and back, or a counter that is incremented thousands of times per second.)
Another question is if we should eagerly or lazily update each key?
Even though several sources could potentially define thousands of key-value pairs, that doesn't necessarily imply that those values are always read or that they are refreshed at the same interval.
Eagerly establishing source-ownership and updating the effective value of every key would probably be costly (performance-wise).
On the other hand, lazily determining which source owns each key at reading time, retrieving the effective value, and finally, type casting it would result in slower reads.
The implementation will need an efficient way to keep track of key ownership.
For example, when a value is required, it would be best if the registry could go directly to the source that owns that key, instead of having first to find (iterate over all sources, in O(N) time) the appropriate source. Respectively, when a source's values are updated, it also needs to efficiently publish updates to all prop objects it owns while skipping ones it does not.
I could write more, but for now, this article would have hopefully set the stage for what this library will do.
Rather than outlining every possible feature ahead of time, I prefer developing a series of articles as I get more and more into the code. Who knows, I may even change my mind about some of these things along the way!
I plan on (re)writing this Java library from scratch and documenting it all, including topics such as:
- setting up a GitHub repository
- configuring a build system
- configuring a CI/CD pipeline
- writing performant, well-tested code
- pushing releases to a public Maven repo (e.g., Maven Central)
- formatting the code to a common standard
- linting the code
- benchmarking and documenting a performance baseline
- and others
Stay tuned for more, and don't forget to subscribe to my newsletter if you want to receive all updates about this series by email!
Until next time...