Loading a configuration in Scala

For reading a configuration in Scala, TypeSafe Config is often used. This blog post discusses the  problems with TypeSafe Config and a better alternative, PureConfig.

As an example I’m using the following small configuration file:

database {
  schema = "jdbc:postgresql://localhost:5432/my_database"
  user = "username"
  password = "pw"
}

TypeSafe Config

TypeSafe Config is a well known and often used library to read configuration files for an application. The disadvantage of this library is that you need to read the individual configuration parameters manually and you can get an error when you read a configuration parameter. If you don’t read all configuration parameters at the start of your program, you could get an exception when reading a configuration parameter during running your program.

This is how you would read the sample configuration file using TypeSafe Config:

val config = ConfigFactory.load()
val schema = config.getString("database.schema")
val user = config.getString("database.user")
val password = config.getString("database.password")

println(s"schema $schema")
println(s"user $user")
println(s"password $password")

PureConfig

PureConfig reads TypeSafe Config’s configuration files, so both libraries use the same configuration file format, only PureConfig makes it easier to read your configuration and to detect errors in your configuration: you define case classes matching your configuration and use pureconfig.loadConfig[Config] where Config is your configuration case class to read the configuration.

Reading the configuration in this way when starting your program will get an Either of configuration failures or your Config case class. Reading the configuration in this way you can ensure that the configuration is as expected before starting the rest of your program.

This is how you would read the sample configuration file using PureConfig:

case class DatabaseConfig(schema: String, user: String, password: String)

case class Config(database: DatabaseConfig)

pureconfig.loadConfig[Config] match {
  case Left(configReaderFailures) =>
    sys.error(s"Encountered the following errors reading the configuration: ${configReaderFailures.toList.mkString("\n")}")
  case Right(config) =>
    println(s"schema ${config.database.schema}")
    println(s"user ${config.database.user}")
    println(s"password ${config.database.password}")
}

Configuration for different environments

You typically have different configurations for different environments. E.g. the host for your database and username and password for it usually differ for a development and a production environment. You can customize a configuration for a different environment in two different ways:

  • Use a different configuration file per environment to override default values.
  • Use environment variables to override default values.

Using a different configuration file per environment

Use a reference.conf file in your project and create an application.conf (or named differently) for your different environments. The first line of your configuration should be include "reference.conf". This loads the default values from the reference.conf file so you only need to override values when the default values aren’t appropriate.

Here’s an example that overrides the database password:

include "reference.conf"

database.password="?*%b~gA-}HT!b=3'"

To use the configuration file, set the config.file system property to the path of your configuration file when you run the application, e.g.:

sbt -Dconfig.file=application.conf run

Local development

For local development you can create a file with your overrides and use the config.file property to use it. If everybody working on the project uses the same name for this file (e.g. development.conf), you can add it to .gitignore to prevent that it accidentally ends up in git.

Use environment variables to override default values

Use again a reference.conf file in your project and include for the settings that possibly need be overridden an environment variable, which if present, will override the default setting, e.g.:

password = "pw"
password = ${?DATABASE_PASSWORD}

The question mark before DATABASE_PASSWORD makes the presence of the environment variable optional. So if the environment variable DATABASE_PASSWORD is present, it will be used, otherwise the default value pw will be used.

Before starting the program, make sure the environment variables have been defined, e.g. (for Linux):

DATABASE_PASSWORD=secret
export DATABASE_PASSWORD
sbt run

Omitting the question mark is generally not a good idea because this forces someone who wants to run the application locally to set environment variables. It is better to always have sensible defaults.

Local development

For local development you can use the sbt plugin sbt-dotenv. This allows you to create a .env file in which you set your environment variables. Adding this file to .gitignore ensures that is doesn’t accidentally ends up in git. It is a good practise to provide a .env.sample file with the environment variables and the default settings so every developer can use this to create his or her .env file.

Combining both methods

Sometimes when you use a different configuration file per environment you also may want to use environment variables because you don’t want passwords to be available in plain text in a configuration file, but read these from a vault and set these through environment variables.

Code example

A project with sample code is available at https://github.com/jaspervz/configexample

Leave a Reply

Your email address will not be published.