You don't need a framework for configuration. There are plenty of configuration libraries that the frameworks use directly.

Typesafe Config

A common use case for configs is to have defaults (application.conf) and overrides (application.{env}.conf) per environment. Typesafe Config is great for this use case. Here is a mini wrapper for building Typesafe Config objects.

public class Configs {
    private static final Logger log = LoggerFactory.getLogger(Configs.class);

    private Configs() { }

     * I am letting the typesafe configs bleed out on purpose here.
     * We could abstract out and delegate but its not worth it.
     * I am gambling on the fact that I will not switch out the config library.
    private static final Config system = ConfigFactory.systemProperties();
    private static final Config properties = new Builder().withSecureConf().envAwareApp().build();

    public static Config system() {
        return system;

    public static Config properties() {
        return properties;

    // This should return the current executing user path
    public static String getExecutionDirectory() {
        return system.getString("user.dir");

    public static Map<String, Object> asMap(Config config) {
        return Seq.seq(config.entrySet())
                  .toMap(e -> e.getKey(), e -> e.getValue().unwrapped());

    public static <T> T getOrDefault(Config config, String path, BiFunction<Config, String, T> extractor, T defaultValue) {
        if (config.hasPath(path)) {
            return extractor.apply(config, path);
        return defaultValue;

    public static <T> T getOrDefault(Config config, String path, BiFunction<Config, String, T> extractor, Supplier<T> defaultSupplier) {
        if (config.hasPath(path)) {
            return extractor.apply(config, path);
        return defaultSupplier.get();

    public static class Builder {
        private Config conf;

        public Builder() {
  "Loading configs first row is highest priority, second row is fallback and so on");

        public Builder withResource(String resource) {
            conf = returnOrFallback(ConfigFactory.parseResources(resource));
  "Loaded config file from resource ({})", resource);
            return this;

        public Builder withOptionalFile(String path) {
            File secureConfFile = new File(path);
            if (secureConfFile.exists()) {
      "Loaded config file from path ({})", path);
                conf = returnOrFallback(ConfigFactory.parseFile(secureConfFile));
            } else {
      "Attempted to load file from path ({}) but it was not found", path);
            return this;

        public Builder envAwareApp() {
            String env = system.hasPath("env") ? system.getString("env") : "local";
            String envFile = "application." + env + ".conf";
            return withResource(envFile).withResource("application.conf");

        public Builder withSecureConf() {
            return withOptionalFile(getExecutionDirectory() + "/secure.conf");

        public Config build() {
            // Resolve substitutions.
            conf = conf.resolve();
            if (log.isDebugEnabled()) {
                log.debug("Logging properties. Make sure sensitive data such as passwords or secrets are not logged!");
            return conf;

        private Config returnOrFallback(Config config) {
            if (this.conf == null) {
                return config;
            return this.conf.withFallback(config);

    public static void main(String[] args) {;

From this code you have two global configurations set up.

  • System - This has tons of useful JVM properties as well as any -D params you pass to the JVM.
  • Properties - All of your application.{env}.conf and application.conf properties merged.
    • application.conf - All general configs and defaults.
    • application.local.conf - Overrides for local env. .gitignore this file so each developer can have their own version.
    • - Prod config, to use it just pass -Denv=prod to the JVM.