Static Service Configuration

May 30, 2019
programming

Static Service Configuration

Configuration is an essential part of building a web service, as it is what allows behavior to change without writing new code as you operate that service. There’s already a lot on the internet about how to inject configuration values into an application process (and specifically service discovery and secrets management). I’m instead going to cover a few basic heuristics on how to organize your application code that receives static configuration - that is, configuration that is modified while the service is running.

What falls into this bucket?

Things not falling into this bucket could include things like routing configurations for a database migration, or other such values that you may wish to modify on a more dynamic basis.

Heuristic 1: Injection

No matter how the configuration values are ultimately passed into your service’s process (whether they’re written to files on disk, injected into environment variables, or dynamically accessed from a configuration service), code that requires static configuration should have configuration values passed in to function scope instead of allowing functions to arbitrarily access globals from anywhere. It’s the difference between:

def readfromdb():
    db.init(System.getenv("DATABASE_URL"), System.getenv("DATABASE_USER"), System.getenv("DATABASE_PASSWORD"))
    return db.query(...)

# and

def readfromdb(DatabaseConfig dbConfig):
    db.init(dbConfig.url, dbConfig.user, dbConfig.pass)
    return db.query(...)

The latter method is easier to test (you don’t have to set environment variables before running tests) and easier to maintain (you don’t have to remember which functions have random environment globals you need to keep track of).

Heuristic 2: Single Point of Config

So you’ve re-written everything so that config gets passed in, but ultimately, something still has to read the config values. The second heuristic is simple - have a single routine read in all of your relevant config (whether it’s a file, environment variables, remote service, or a mix of all 3) and hydrate a single object or datum (which of course may have nested types):

object DictionaryApp extends App {
  ...

  val config = DictionaryConfig.load()
  val dictionarySystem = new DictionarySystem(config.size)
  val bindingFuture = Http().bindAndHandle(dictionarySystem, config.http.host, config.http.port)
}

case class DictionaryConfig(HttpConfig http, size: Int)
case class HttpConfig(host: String, port: Int)
object DictionaryConfig {
  def load(): DictionaryConfig = loadConfig[DictionaryConfig]
}

Ideally, this occurs during your application start-up and is then passed into all of the injection points you’ve already created.

This gives you a standard place to handle config, and provides the added benefit of ensuring that any configuration issues are discovered on application start-up so that your service should fail without beginning to serve real traffic, whereas lazily loading different config values may result in misconfigurations not being discovered until a rarely called endpoint is hit.

Heuristic 3: Minimize Switching

The last intuition is that you want to minimize the surface area of code you write that behaves differently based on configuration values. The most common example is having a bunch of switch statements throughout your codebase that cause different behavior based on what environment you’re in.

Whenever possible, push these switch statements out of your code entirely. A very basic approach to having two services communicate with each other in different environments (development, staging, production) might involve:

http.request.get(switch (config.env) {
    case PRODUCTION  => "http://produrl"
    case STAGING     => "http://stagingurl"
    case DEVELOPMENT => "http://devurl"
})

Of course, only doing this in a single place in the codebase is the first step making this easier to maintain, then pushing it out of code and into your configuration by having a config.url attribute get hydrated with the right URL is even better. Service Discovery pushes this to the next level when there’s a need for these URLs to be dynamic.

Of course, not all such opportunities will be so clear-cut, but you can still bias towards having a single place in your codebase handle that switching logic. When combined with a polymorphic interface, this can often result in configurable behavior with minimal impact on the rest of your codebase.

Errors and Partial Functions

July 10, 2019
programming

Conceptual Arbitrariness

May 15, 2019
programming

Algebra For Accounting

March 27, 2019
programming