You may know Eevee, my newest Discord bot written in Java. Eevee has been growing pretty large lately in terms of codebase and with how much of a pain it is to manage configuration between different environments (development and production mostly) I thought it was finally time simplify the way I manage Eevee's configuration.

In Comes Coffee...

Coffee is a super-fast configuration management system that allows me to change Eevee's configuration on the fly. Typically a change in Coffee will be reflected in Eevee within 30 seconds or less. Essentially, Coffee is configuration as a service.

The way Coffee works is that all configuration values are stored in memory and updates are written through to disk. Reads are read straight from memory so getting configuration values are super quick. Of course, because Eevee often relies on numerous configurations per command invocation, the Coffee client library is smart enough to cache config values for some duration before reading from the Coffee server. This lessens the load on the Coffee server as multiple Eevee shards read from the same server.

Data Organization in Coffee

Values are stored in Coffee as a tree of nodes where each node contains a key and a value. You can easily lookup any configuration value by specifying a list of key parts such as ["eevee", "twitter", "apiConsumerKey"].

Because configuration values are keyed in a hierachical manner, Coffee also allows you to do some fancy things like retrieve config values based off of an application runtime context. Let's say you have a key such as ["server", "databaseHost"]. In many cases, this key should return a different value depending on whether or not your application is running in a development environment or production. By supplying an application's runtime context to Coffee when reading, Coffee is able to intelligently return the correct value based off of context matchers.

Normal consumers of Coffee will not need to know the exact data representation of Coffee nodes but for completion's sake here is an example of what a node may look like:

{
    "key": "myKey",
    "value": [
        {
            "matches": [
                {
                    "env": "prod",
                    "_coffee_node_value-fe48d9d3": "myKeyValueOne"
                },
                {
                    "env": "dev",
                    "hostname": "dev.mywebsite.com",
                    "_coffee_node_value-e29d1906": "myKeyValueTwo"
                }
            ]
        }
    ],
    "children": []
}

Context matchers will match only if all the values specified match. Coffee does not yet support the or or any operator for matching any value of many.

Drinking Coffee

Once you've put some config values into Coffee you probably want to use them. Coffee allows consumers to interact with it through the Coffee client library powered by gRPC. Since Coffee uses gRPC, it uses protocol buffers to efficiently communicate with clients. Consumers would send something like this...

message Key {
    repeated string value = 1;
    map<string, string> context = 2;
}

And get something back like this...

message StringEntry {
    string error = 1;
    repeated string key = 2;
    string value = 3;
}

Where the entry type is dynamically determined based on the entry value at runtime.

Consumers don't need to worry about the actual schema though—it's all handled by the Coffee client library. Clients just need to call something like:

int successEmbedColorDecimal = (int) coffeeClient.getNumber(
    "eevee", "successEmbedColorDecimal"
);