Introduction
Dhall is a programmable configuration language that provides type safety, templating, and functional programming features for configuration management. When integrated with Java applications, it offers robust, type-safe configuration management with excellent maintainability and reusability.
Architecture Overview
[Dhall Files] → [Dhall Java Binding] → [Java Objects] → [Application] ↓ ↓ ↓ ↓ Type-Safe Parser & Configuration Spring Boot Configuration Evaluator Beans MicroProfile Templates Validation Validation Custom Apps Imports Type Checking Dependency Injection
Step 1: Project Dependencies
Maven Configuration
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>dhall-java-config</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<!-- Dhall Java -->
<dhall.version>1.40.0</dhall.version>
<spring-boot.version>3.2.0</spring-boot.version>
<!-- JSON Processing -->
<jackson.version>2.15.0</jackson.version>
</properties>
<dependencies>
<!-- Dhall Java Core -->
<dependency>
<groupId>org.dhall</groupId>
<artifactId>dhall-core</artifactId>
<version>${dhall.version}</version>
</dependency>
<!-- Dhall Java Parser -->
<dependency>
<groupId>org.dhall</groupId>
<artifactId>dhall-parser</artifactId>
<version>${dhall.version}</version>
</dependency>
<!-- Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<version>${spring-boot.version}</version>
<optional>true</optional>
</dependency>
<!-- JSON Processing -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>${jackson.version}</version>
</dependency>
<!-- YAML Support -->
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-yaml</artifactId>
<version>${jackson.version}</version>
</dependency>
<!-- Testing -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>${spring-boot.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.10.0</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring-boot.version}</version>
</plugin>
</plugins>
</build>
</project>
Step 2: Dhall Configuration Structure
Project Directory Structure
src/ ├── main/ │ ├── java/ │ │ └── com/example/dhall/ │ └── resources/ │ └── config/ │ ├── base.dhall │ ├── database.dhall │ ├── server.dhall │ ├── security.dhall │ ├── logging.dhall │ ├── features.dhall │ ├── environments/ │ │ ├── development.dhall │ │ ├── staging.dhall │ │ └── production.dhall │ └── application.dhall └── test/ └── resources/ └── config/ └── test.dhall
Step 3: Core Dhall Configuration Files
Base Configuration
src/main/resources/config/base.dhall
-- Base configuration types and defaults
let Config =
{ database : ./database.dhall
, server : ./server.dhall
, security : ./security.dhall
, logging : ./logging.dhall
, features : ./features.dhall
, environment : Text
}
let defaultConfig =
{ database = ./database.dhall.default
, server = ./server.dhall.default
, security = ./security.dhall.default
, logging = ./logging.dhall.default
, features = ./features.dhall.default
, environment = "development"
}
in { Type = Config, default = defaultConfig }
Database Configuration
src/main/resources/config/database.dhall
-- Database configuration type
let DatabaseConfig =
{ url : Text
, username : Text
, password : Text
, driver : Text
, pool :
{ minSize : Natural
, maxSize : Natural
, connectionTimeout : Natural
, idleTimeout : Natural
}
, migrations : { enabled : Bool, locations : List Text }
}
let defaultDatabase =
{ url = "jdbc:postgresql://localhost:5432/mydb"
, username = "app_user"
, password = ""
, driver = "org.postgresql.Driver"
, pool =
{ minSize = 2
, maxSize = 10
, connectionTimeout = 30000
, idleTimeout = 600000
}
, migrations = { enabled = True, locations = [ "classpath:db/migration" ] }
}
in { Type = DatabaseConfig, default = defaultDatabase }
Server Configuration
src/main/resources/config/server.dhall
-- Server configuration
let ServerConfig =
{ port : Natural
, contextPath : Text
, compression : { enabled : Bool, minSize : Natural }
, ssl : { enabled : Bool, keyStore : Optional Text, keyStorePassword : Optional Text }
, cors :
{ allowedOrigins : List Text
, allowedMethods : List Text
, allowedHeaders : List Text
}
}
let defaultServer =
{ port = 8080
, contextPath = "/"
, compression = { enabled = True, minSize = 1024 }
, ssl = { enabled = False, keyStore = None Text, keyStorePassword = None Text }
, cors =
{ allowedOrigins = [ "http://localhost:3000" ]
, allowedMethods = [ "GET", "POST", "PUT", "DELETE", "OPTIONS" ]
, allowedHeaders = [ "*" ]
}
}
in { Type = ServerConfig, default = defaultServer }
Security Configuration
src/main/resources/config/security.dhall
-- Security configuration
let SecurityConfig =
{ jwt :
{ secret : Text
, expiration : Natural
, issuer : Text
}
, oauth2 :
{ enabled : Bool
, clients :
List
{ clientId : Text
, clientSecret : Text
, redirectUris : List Text
, scopes : List Text
}
}
, rateLimiting :
{ enabled : Bool
, requestsPerMinute : Natural
, burstCapacity : Natural
}
}
let defaultSecurity =
{ jwt =
{ secret = "default-secret-key-change-in-production"
, expiration = 86400
, issuer = "myapp"
}
, oauth2 =
{ enabled = False
, clients = [] : List { clientId : Text, clientSecret : Text, redirectUris : List Text, scopes : List Text }
}
, rateLimiting = { enabled = True, requestsPerMinute = 100, burstCapacity = 50 }
}
in { Type = SecurityConfig, default = defaultSecurity }
Logging Configuration
src/main/resources/config/logging.dhall
-- Logging configuration
let LoggingConfig =
{ level : ./logging/levels.dhall
, appenders :
List
{ type : Text
, pattern : Text
, file : Optional Text
, maxHistory : Optional Natural
, maxFileSize : Optional Text
}
, packages :
List { package : Text, level : ./logging/levels.dhall }
}
let defaultLogging =
{ level = ./logging/levels.dhall.INFO
, appenders =
[ { type = "console"
, pattern = "%d{yyyy-MM-dd HH:mm:ss} - %logger{36} - %msg%n"
, file = None Text
, maxHistory = None Natural
, maxFileSize = None Text
}
]
, packages =
[ { package = "com.example", level = ./logging/levels.dhall.DEBUG }
, { package = "org.springframework", level = ./logging/levels.dhall.INFO }
]
}
in { Type = LoggingConfig, default = defaultLogging }
Logging Levels
src/main/resources/config/logging/levels.dhall
-- Logging levels as a union type
let Level = < ERROR | WARN | INFO | DEBUG | TRACE >
in { Type = Level
, ERROR = Level.ERROR
, WARN = Level.WARN
, INFO = Level.INFO
, DEBUG = Level.DEBUG
, TRACE = Level.TRACE
}
Feature Flags Configuration
src/main/resources/config/features.dhall
-- Feature flags configuration
let FeatureConfig =
{ darkLaunch :
List
{ feature : Text
, enabled : Bool
, rolloutPercentage : Double
, targetUsers : List Text
}
, abTesting :
List
{ experiment : Text
, variants : List { name : Text, weight : Double }
, enabled : Bool
}
, operational :
{ maintenanceMode : Bool
, readOnlyMode : Bool
, rateLimitEnabled : Bool
}
}
let defaultFeatures =
{ darkLaunch =
[ { feature = "new-payment-processor"
, enabled = False
, rolloutPercentage = 0.0
, targetUsers = [] : List Text
}
, { feature = "enhanced-search"
, enabled = True
, rolloutPercentage = 50.0
, targetUsers = [ "premium" ]
}
]
, abTesting =
[ { experiment = "pricing-page"
, variants =
[ { name = "control", weight = 50.0 }
, { name = "variant-a", weight = 25.0 }
, { name = "variant-b", weight = 25.0 }
]
, enabled = True
}
]
, operational =
{ maintenanceMode = False
, readOnlyMode = False
, rateLimitEnabled = True
}
}
in { Type = FeatureConfig, default = defaultFeatures }
Step 4: Environment-Specific Configurations
Development Environment
src/main/resources/config/environments/development.dhall
-- Development environment configuration
let base = ../base.dhall
let developmentConfig =
base.default
⫽ { database =
base.default.database
⫽ { url = "jdbc:postgresql://localhost:5432/dev_db"
, username = "dev_user"
, pool = base.default.database.pool ⫽ { minSize = 1, maxSize = 5 }
}
, server = base.default.server ⫽ { port = 8081 }
, logging =
base.default.logging
⫽ { level = ../logging/levels.dhall.DEBUG
, packages =
base.default.logging.pages
# [ { package = "com.example", level = ../logging/levels.dhall.TRACE } ]
}
, environment = "development"
}
in developmentConfig
Production Environment
src/main/resources/config/environments/production.dhall
-- Production environment configuration
let base = ../base.dhall
let productionConfig =
base.default
⫽ { database =
base.default.database
⫽ { url = "jdbc:postgresql://prod-db.cluster.local:5432/prod_db"
, username = "prod_user"
, pool = base.default.database.pool ⫽ { minSize = 5, maxSize = 20 }
}
, server = base.default.server ⫽ { port = 8080 }
, security =
base.default.security
⫽ { jwt = base.default.security.jwt ⫽ { secret = "super-secure-production-secret" } }
, logging =
base.default.logging
⫽ { level = ../logging/levels.dhall.WARN
, appenders =
base.default.logging.appenders
# [ { type = "file"
, pattern = "%d{yyyy-MM-dd HH:mm:ss} - %logger{36} - %msg%n"
, file = Some Text "/var/log/app/app.log"
, maxHistory = Some Natural 30
, maxFileSize = Some Text "100MB"
}
]
}
, environment = "production"
}
in productionConfig
Main Application Configuration
src/main/resources/config/application.dhall
-- Main application configuration that selects environment based on ENV variable let envVar = env:ENV as Text let environmentConfig = if envVar == "production" then ./environments/production.dhall else if envVar == "staging" then ./environments/staging.dhall else ./environments/development.dhall in environmentConfig
Step 5: Java Configuration Models
Base Configuration Classes
DatabaseConfig.java
package com.example.dhall.config.model;
import com.fasterxml.jackson.annotation.JsonProperty;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Positive;
import java.util.List;
public class DatabaseConfig {
@NotBlank
private String url;
@NotBlank
private String username;
@NotBlank
private String password;
@NotBlank
private String driver;
@Valid
@NotNull
private PoolConfig pool;
@Valid
@NotNull
private MigrationConfig migrations;
// Getters and Setters
public String getUrl() { return url; }
public void setUrl(String url) { this.url = url; }
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getPassword() { return password; }
public void setPassword(String password) { this.password = password; }
public String getDriver() { return driver; }
public void setDriver(String driver) { this.driver = driver; }
public PoolConfig getPool() { return pool; }
public void setPool(PoolConfig pool) { this.pool = pool; }
public MigrationConfig getMigrations() { return migrations; }
public void setMigrations(MigrationConfig migrations) { this.migrations = migrations; }
public static class PoolConfig {
@Positive
private int minSize;
@Positive
private int maxSize;
@Positive
private long connectionTimeout;
@Positive
private long idleTimeout;
// Getters and Setters
public int getMinSize() { return minSize; }
public void setMinSize(int minSize) { this.minSize = minSize; }
public int getMaxSize() { return maxSize; }
public void setMaxSize(int maxSize) { this.maxSize = maxSize; }
public long getConnectionTimeout() { return connectionTimeout; }
public void setConnectionTimeout(long connectionTimeout) { this.connectionTimeout = connectionTimeout; }
public long getIdleTimeout() { return idleTimeout; }
public void setIdleTimeout(long idleTimeout) { this.idleTimeout = idleTimeout; }
}
public static class MigrationConfig {
private boolean enabled;
@NotNull
private List<String> locations;
// Getters and Setters
public boolean isEnabled() { return enabled; }
public void setEnabled(boolean enabled) { this.enabled = enabled; }
public List<String> getLocations() { return locations; }
public void setLocations(List<String> locations) { this.locations = locations; }
}
}
ServerConfig.java
package com.example.dhall.config.model;
import com.fasterxml.jackson.annotation.JsonProperty;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Positive;
import java.util.List;
import java.util.Optional;
public class ServerConfig {
@Positive
private int port;
private String contextPath;
@Valid
@NotNull
private CompressionConfig compression;
@Valid
@NotNull
private SslConfig ssl;
@Valid
@NotNull
private CorsConfig cors;
// Getters and Setters
public int getPort() { return port; }
public void setPort(int port) { this.port = port; }
public String getContextPath() { return contextPath; }
public void setContextPath(String contextPath) { this.contextPath = contextPath; }
public CompressionConfig getCompression() { return compression; }
public void setCompression(CompressionConfig compression) { this.compression = compression; }
public SslConfig getSsl() { return ssl; }
public void setSsl(SslConfig ssl) { this.ssl = ssl; }
public CorsConfig getCors() { return cors; }
public void setCors(CorsConfig cors) { this.cors = cors; }
public static class CompressionConfig {
private boolean enabled;
@Positive
private long minSize;
// Getters and Setters
public boolean isEnabled() { return enabled; }
public void setEnabled(boolean enabled) { this.enabled = enabled; }
public long getMinSize() { return minSize; }
public void setMinSize(long minSize) { this.minSize = minSize; }
}
public static class SslConfig {
private boolean enabled;
private Optional<String> keyStore;
private Optional<String> keyStorePassword;
// Getters and Setters
public boolean isEnabled() { return enabled; }
public void setEnabled(boolean enabled) { this.enabled = enabled; }
public Optional<String> getKeyStore() { return keyStore; }
public void setKeyStore(Optional<String> keyStore) { this.keyStore = keyStore; }
public Optional<String> getKeyStorePassword() { return keyStorePassword; }
public void setKeyStorePassword(Optional<String> keyStorePassword) { this.keyStorePassword = keyStorePassword; }
}
public static class CorsConfig {
@NotNull
private List<String> allowedOrigins;
@NotNull
private List<String> allowedMethods;
@NotNull
private List<String> allowedHeaders;
// Getters and Setters
public List<String> getAllowedOrigins() { return allowedOrigins; }
public void setAllowedOrigins(List<String> allowedOrigins) { this.allowedOrigins = allowedOrigins; }
public List<String> getAllowedMethods() { return allowedMethods; }
public void setAllowedMethods(List<String> allowedMethods) { this.allowedMethods = allowedMethods; }
public List<String> getAllowedHeaders() { return allowedHeaders; }
public void setAllowedHeaders(List<String> allowedHeaders) { this.allowedHeaders = allowedHeaders; }
}
}
FeatureConfig.java
package com.example.dhall.config.model;
import com.fasterxml.jackson.annotation.JsonProperty;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import java.util.List;
public class FeatureConfig {
@Valid
@NotNull
private List<DarkLaunchFeature> darkLaunch;
@Valid
@NotNull
private List<AbTest> abTesting;
@Valid
@NotNull
private OperationalFeatures operational;
// Getters and Setters
public List<DarkLaunchFeature> getDarkLaunch() { return darkLaunch; }
public void setDarkLaunch(List<DarkLaunchFeature> darkLaunch) { this.darkLaunch = darkLaunch; }
public List<AbTest> getAbTesting() { return abTesting; }
public void setAbTesting(List<AbTest> abTesting) { this.abTesting = abTesting; }
public OperationalFeatures getOperational() { return operational; }
public void setOperational(OperationalFeatures operational) { this.operational = operational; }
public static class DarkLaunchFeature {
private String feature;
private boolean enabled;
private double rolloutPercentage;
private List<String> targetUsers;
// Getters and Setters
public String getFeature() { return feature; }
public void setFeature(String feature) { this.feature = feature; }
public boolean isEnabled() { return enabled; }
public void setEnabled(boolean enabled) { this.enabled = enabled; }
public double getRolloutPercentage() { return rolloutPercentage; }
public void setRolloutPercentage(double rolloutPercentage) { this.rolloutPercentage = rolloutPercentage; }
public List<String> getTargetUsers() { return targetUsers; }
public void setTargetUsers(List<String> targetUsers) { this.targetUsers = targetUsers; }
}
public static class AbTest {
private String experiment;
private List<Variant> variants;
private boolean enabled;
// Getters and Setters
public String getExperiment() { return experiment; }
public void setExperiment(String experiment) { this.experiment = experiment; }
public List<Variant> getVariants() { return variants; }
public void setVariants(List<Variant> variants) { this.variants = variants; }
public boolean isEnabled() { return enabled; }
public void setEnabled(boolean enabled) { this.enabled = enabled; }
public static class Variant {
private String name;
private double weight;
// Getters and Setters
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public double getWeight() { return weight; }
public void setWeight(double weight) { this.weight = weight; }
}
}
public static class OperationalFeatures {
private boolean maintenanceMode;
private boolean readOnlyMode;
private boolean rateLimitEnabled;
// Getters and Setters
public boolean isMaintenanceMode() { return maintenanceMode; }
public void setMaintenanceMode(boolean maintenanceMode) { this.maintenanceMode = maintenanceMode; }
public boolean isReadOnlyMode() { return readOnlyMode; }
public void setReadOnlyMode(boolean readOnlyMode) { this.readOnlyMode = readOnlyMode; }
public boolean isRateLimitEnabled() { return rateLimitEnabled; }
public void setRateLimitEnabled(boolean rateLimitEnabled) { this.rateLimitEnabled = rateLimitEnabled; }
}
}
AppConfig.java
package com.example.dhall.config.model;
import com.fasterxml.jackson.annotation.JsonProperty;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
public class AppConfig {
@Valid
@NotNull
private DatabaseConfig database;
@Valid
@NotNull
private ServerConfig server;
@Valid
@NotNull
private SecurityConfig security;
@Valid
@NotNull
private LoggingConfig logging;
@Valid
@NotNull
private FeatureConfig features;
@NotNull
private String environment;
// Getters and Setters
public DatabaseConfig getDatabase() { return database; }
public void setDatabase(DatabaseConfig database) { this.database = database; }
public ServerConfig getServer() { return server; }
public void setServer(ServerConfig server) { this.server = server; }
public SecurityConfig getSecurity() { return security; }
public void setSecurity(SecurityConfig security) { this.security = security; }
public LoggingConfig getLogging() { return logging; }
public void setLogging(LoggingConfig logging) { this.logging = logging; }
public FeatureConfig getFeatures() { return features; }
public void setFeatures(FeatureConfig features) { this.features = features; }
public String getEnvironment() { return environment; }
public void setEnvironment(String environment) { this.environment = environment; }
}
Step 6: Dhall Configuration Loader
Core Dhall Loader
DhallConfigLoader.java
package com.example.dhall.config;
import com.example.dhall.config.model.AppConfig;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.dhall.Dhall;
import org.dhall.ImportException;
import org.dhall.core.Expr;
import org.dhall.core.constant.Type;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Optional;
public class DhallConfigLoader {
private static final Logger logger = LoggerFactory.getLogger(DhallConfigLoader.class);
private final Dhall dhall;
private final ObjectMapper objectMapper;
public DhallConfigLoader() {
this.dhall = new Dhall();
this.objectMapper = new ObjectMapper();
}
public DhallConfigLoader(ObjectMapper objectMapper) {
this.dhall = new Dhall();
this.objectMapper = objectMapper;
}
/**
* Load configuration from a Dhall file on classpath
*/
public AppConfig loadConfig(String configPath) throws ConfigLoadException {
try {
Resource resource = new ClassPathResource(configPath);
if (!resource.exists()) {
throw new ConfigLoadException("Configuration file not found: " + configPath);
}
String dhallContent = Files.readString(Path.of(resource.getURI()));
return loadConfigFromString(dhallContent);
} catch (IOException e) {
throw new ConfigLoadException("Failed to read configuration file: " + configPath, e);
}
}
/**
* Load configuration from Dhall string content
*/
public AppConfig loadConfigFromString(String dhallContent) throws ConfigLoadException {
try {
// Parse and evaluate Dhall expression
Expr<Object, Object> expr = dhall.getParser().parse(dhallContent);
Expr<Object, Object> normalized = dhall.getNormalizer().normalize(expr);
// Convert to JSON representation
String json = dhall.getEncoder().encode(normalized);
// Parse JSON into Java objects
return objectMapper.readValue(json, AppConfig.class);
} catch (ImportException e) {
throw new ConfigLoadException("Failed to import Dhall dependencies", e);
} catch (Exception e) {
throw new ConfigLoadException("Failed to load configuration from Dhall", e);
}
}
/**
* Load configuration with environment variable substitution
*/
public AppConfig loadConfigWithEnv(String configPath) throws ConfigLoadException {
try {
// Set environment variables for Dhall
System.setProperty("dhall.trust.env", "true");
Resource resource = new ClassPathResource(configPath);
String dhallContent = Files.readString(Path.of(resource.getURI()));
return loadConfigFromString(dhallContent);
} catch (IOException e) {
throw new ConfigLoadException("Failed to read configuration file: " + configPath, e);
}
}
/**
* Validate Dhall configuration type
*/
public boolean validateConfigType(String configPath, String expectedType) throws ConfigLoadException {
try {
Resource resource = new ClassPathResource(configPath);
String dhallContent = Files.readString(Path.of(resource.getURI()));
Expr<Object, Object> expr = dhall.getParser().parse(dhallContent);
Expr<Object, Object> typeExpr = dhall.getTypeChecker().typeCheck(expr);
return typeExpr.equals(Type.ANY);
} catch (Exception e) {
throw new ConfigLoadException("Failed to validate configuration type", e);
}
}
public static class ConfigLoadException extends Exception {
public ConfigLoadException(String message) {
super(message);
}
public ConfigLoadException(String message, Throwable cause) {
super(message, cause);
}
}
}
Spring Configuration Properties
DhallConfigurationProperties.java
package com.example.dhall.config;
import com.example.dhall.config.model.AppConfig;
import jakarta.annotation.PostConstruct;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.validation.annotation.Validated;
@Configuration
@ConfigurationProperties(prefix = "app.dhall")
@Validated
public class DhallConfigurationProperties {
private static final Logger logger = LoggerFactory.getLogger(DhallConfigurationProperties.class);
private String configPath = "config/application.dhall";
private boolean enabled = true;
private boolean validateOnStartup = true;
private boolean cacheConfig = true;
private AppConfig config;
private final DhallConfigLoader configLoader;
public DhallConfigurationProperties(DhallConfigLoader configLoader) {
this.configLoader = configLoader;
}
@PostConstruct
public void init() {
if (!enabled) {
logger.info("Dhall configuration is disabled");
return;
}
try {
logger.info("Loading Dhall configuration from: {}", configPath);
if (validateOnStartup) {
boolean isValid = configLoader.validateConfigType(configPath, "Config");
if (!isValid) {
throw new RuntimeException("Dhall configuration type validation failed");
}
}
this.config = configLoader.loadConfigWithEnv(configPath);
logger.info("Successfully loaded Dhall configuration for environment: {}",
config.getEnvironment());
} catch (DhallConfigLoader.ConfigLoadException e) {
throw new RuntimeException("Failed to load Dhall configuration", e);
}
}
// Getters and Setters
public String getConfigPath() { return configPath; }
public void setConfigPath(String configPath) { this.configPath = configPath; }
public boolean isEnabled() { return enabled; }
public void setEnabled(boolean enabled) { this.enabled = enabled; }
public boolean isValidateOnStartup() { return validateOnStartup; }
public void setValidateOnStartup(boolean validateOnStartup) { this.validateOnStartup = validateOnStartup; }
public boolean isCacheConfig() { return cacheConfig; }
public void setCacheConfig(boolean cacheConfig) { this.cacheConfig = cacheConfig; }
public AppConfig getConfig() { return config; }
public void setConfig(AppConfig config) { this.config = config; }
}
Step 7: Spring Boot Integration
Spring Configuration
DhallConfig.java
package com.example.dhall.config;
import com.example.dhall.config.model.AppConfig;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
@Configuration
public class DhallConfig {
@Bean
public ObjectMapper objectMapper() {
ObjectMapper mapper = new ObjectMapper();
mapper.findAndRegisterModules();
mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
return mapper;
}
@Bean
public ObjectMapper yamlObjectMapper() {
YAMLFactory yamlFactory = new YAMLFactory();
yamlFactory.configure(YAMLGenerator.Feature.WRITE_DOC_START_MARKER, false);
yamlFactory.configure(YAMLGenerator.Feature.MINIMIZE_QUOTES, true);
ObjectMapper mapper = new ObjectMapper(yamlFactory);
mapper.findAndRegisterModules();
return mapper;
}
@Bean
public DhallConfigLoader dhallConfigLoader(ObjectMapper objectMapper) {
return new DhallConfigLoader(objectMapper);
}
@Bean
@Primary
public AppConfig appConfig(DhallConfigurationProperties properties) {
return properties.getConfig();
}
}
Configuration Service
ConfigurationService.java
package com.example.dhall.service;
import com.example.dhall.config.model.AppConfig;
import com.example.dhall.config.model.FeatureConfig;
import org.springframework.stereotype.Service;
import java.util.Optional;
@Service
public class ConfigurationService {
private final AppConfig appConfig;
public ConfigurationService(AppConfig appConfig) {
this.appConfig = appConfig;
}
public AppConfig getConfig() {
return appConfig;
}
public FeatureConfig getFeatures() {
return appConfig.getFeatures();
}
public boolean isFeatureEnabled(String featureName) {
return appConfig.getFeatures().getDarkLaunch().stream()
.filter(f -> f.getFeature().equals(featureName))
.findFirst()
.map(FeatureConfig.DarkLaunchFeature::isEnabled)
.orElse(false);
}
public Optional<FeatureConfig.DarkLaunchFeature> getFeature(String featureName) {
return appConfig.getFeatures().getDarkLaunch().stream()
.filter(f -> f.getFeature().equals(featureName))
.findFirst();
}
public boolean isMaintenanceMode() {
return appConfig.getFeatures().getOperational().isMaintenanceMode();
}
public boolean isReadOnlyMode() {
return appConfig.getFeatures().getOperational().isReadOnlyMode();
}
public String getEnvironment() {
return appConfig.getEnvironment();
}
}
Step 8: Usage Examples
Database Service Using Dhall Config
DatabaseService.java
package com.example.dhall.service;
import com.example.dhall.config.model.AppConfig;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import javax.sql.DataSource;
@Service
public class DatabaseService {
private static final Logger logger = LoggerFactory.getLogger(DatabaseService.class);
private final AppConfig appConfig;
private final DataSource dataSource;
public DatabaseService(AppConfig appConfig) {
this.appConfig = appConfig;
this.dataSource = createDataSource();
}
private DataSource createDataSource() {
var dbConfig = appConfig.getDatabase();
HikariConfig hikariConfig = new HikariConfig();
hikariConfig.setJdbcUrl(dbConfig.getUrl());
hikariConfig.setUsername(dbConfig.getUsername());
hikariConfig.setPassword(dbConfig.getPassword());
hikariConfig.setDriverClassName(dbConfig.getDriver());
// Connection pool configuration from Dhall
hikariConfig.setMinimumIdle(dbConfig.getPool().getMinSize());
hikariConfig.setMaximumPoolSize(dbConfig.getPool().getMaxSize());
hikariConfig.setConnectionTimeout(dbConfig.getPool().getConnectionTimeout());
hikariConfig.setIdleTimeout(dbConfig.getPool().getIdleTimeout());
hikariConfig.setPoolName("AppConnectionPool");
logger.info("Initializing database connection pool for: {}", dbConfig.getUrl());
return new HikariDataSource(hikariConfig);
}
public DataSource getDataSource() {
return dataSource;
}
public boolean runMigrations() {
var migrationConfig = appConfig.getDatabase().getMigrations();
if (!migrationConfig.isEnabled()) {
logger.info("Database migrations are disabled");
return false;
}
logger.info("Running database migrations from locations: {}",
migrationConfig.getLocations());
// Implement migration logic here
return true;
}
}
Feature Flag Service
FeatureFlagService.java
package com.example.dhall.service;
import com.example.dhall.config.model.FeatureConfig;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.ThreadLocalRandom;
@Service
public class FeatureFlagService {
private final ConfigurationService configurationService;
public FeatureFlagService(ConfigurationService configurationService) {
this.configurationService = configurationService;
}
public boolean isFeatureEnabled(String featureName) {
return configurationService.isFeatureEnabled(featureName);
}
public boolean isFeatureEnabledForUser(String featureName, String userId, List<String> userGroups) {
Optional<FeatureConfig.DarkLaunchFeature> featureOpt =
configurationService.getFeature(featureName);
if (featureOpt.isEmpty()) {
return false;
}
FeatureConfig.DarkLaunchFeature feature = featureOpt.get();
// Check if feature is globally enabled
if (!feature.isEnabled()) {
return false;
}
// Check targeted users
if (!feature.getTargetUsers().isEmpty()) {
boolean userInTargetGroup = userGroups.stream()
.anyMatch(feature.getTargetUsers()::contains);
if (!userInTargetGroup) {
return false;
}
}
// Check rollout percentage
if (feature.getRolloutPercentage() < 100.0) {
double random = ThreadLocalRandom.current().nextDouble(100.0);
return random < feature.getRolloutPercentage();
}
return true;
}
public Optional<String> getAbTestVariant(String experimentName) {
return configurationService.getFeatures().getAbTesting().stream()
.filter(exp -> exp.getExperiment().equals(experimentName))
.filter(FeatureConfig.AbTest::isEnabled)
.findFirst()
.flatMap(this::selectVariant);
}
private Optional<String> selectVariant(FeatureConfig.AbTest experiment) {
double random = ThreadLocalRandom.current().nextDouble(100.0);
double cumulativeWeight = 0.0;
for (FeatureConfig.AbTest.Variant variant : experiment.getVariants()) {
cumulativeWeight += variant.getWeight();
if (random < cumulativeWeight) {
return Optional.of(variant.getName());
}
}
return Optional.empty();
}
}
Server Configuration
WebServerConfig.java
package com.example.dhall.config;
import com.example.dhall.config.model.AppConfig;
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class WebServerConfig {
@Bean
public WebServerFactoryCustomizer<TomcatServletWebServerFactory> webServerCustomizer(
AppConfig appConfig) {
return factory -> {
var serverConfig = appConfig.getServer();
factory.setPort(serverConfig.getPort());
factory.setContextPath(serverConfig.getContextPath());
// Configure compression
if (serverConfig.getCompression().isEnabled()) {
factory.addConnectorCustomizers(connector -> {
connector.setProperty("compression", "on");
connector.setProperty("compressionMinSize",
String.valueOf(serverConfig.getCompression().getMinSize()));
});
}
};
}
}
Step 9: Testing
Test Configuration
src/test/resources/config/test.dhall
-- Test environment configuration
let base = ../base.dhall
let testConfig =
base.default
⫽ { database =
base.default.database
⫽ { url = "jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1"
, username = "sa"
, password = ""
, driver = "org.h2.Driver"
, pool = base.default.database.pool ⫽ { minSize = 1, maxSize = 2 }
, migrations = base.default.database.migrations ⫽ { enabled = False }
}
, server = base.default.server ⫽ { port = 0 } -- Random port for tests
, environment = "test"
}
in testConfig
Unit Tests
DhallConfigLoaderTest.java
package com.example.dhall.config;
import com.example.dhall.config.model.AppConfig;
import org.junit.jupiter.api.Test;
import org.springframework.core.io.ClassPathResource;
import static org.junit.jupiter.api.Assertions.*;
class DhallConfigLoaderTest {
private final DhallConfigLoader configLoader = new DhallConfigLoader();
@Test
void testLoadConfigFromFile() throws Exception {
AppConfig config = configLoader.loadConfig("config/test.dhall");
assertNotNull(config);
assertEquals("test", config.getEnvironment());
assertNotNull(config.getDatabase());
assertEquals("jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1", config.getDatabase().getUrl());
assertNotNull(config.getServer());
assertEquals(0, config.getServer().getPort());
}
@Test
void testLoadConfigFromString() throws Exception {
String dhallContent = """
{ database =
{ url = "jdbc:postgresql://localhost/test"
, username = "test"
, password = "test"
, driver = "org.postgresql.Driver"
, pool = { minSize = 1, maxSize = 5, connectionTimeout = 30000, idleTimeout = 600000 }
, migrations = { enabled = True, locations = [ "classpath:db/migration" ] }
}
, server =
{ port = 8080
, contextPath = "/"
, compression = { enabled = True, minSize = 1024 }
, ssl = { enabled = False, keyStore = None Text, keyStorePassword = None Text }
, cors = { allowedOrigins = [ "http://localhost:3000" ], allowedMethods = [ "GET" ], allowedHeaders = [ "*" ] }
}
, security = ./security.dhall.default
, logging = ./logging.dhall.default
, features = ./features.dhall.default
, environment = "test"
}
""";
AppConfig config = configLoader.loadConfigFromString(dhallContent);
assertNotNull(config);
assertEquals("test", config.getEnvironment());
assertEquals(8080, config.getServer().getPort());
assertTrue(config.getServer().getCompression().isEnabled());
}
@Test
void testValidateConfigType() throws Exception {
boolean isValid = configLoader.validateConfigType("config/test.dhall", "Config");
assertTrue(isValid);
}
}
ConfigurationServiceTest.java
package com.example.dhall.service;
import com.example.dhall.config.DhallConfigLoader;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.test.util.ReflectionTestUtils;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
class ConfigurationServiceTest {
private ConfigurationService configurationService;
private DhallConfigLoader configLoader;
@BeforeEach
void setUp() throws Exception {
configLoader = new DhallConfigLoader();
var appConfig = configLoader.loadConfig("config/test.dhall");
configurationService = new ConfigurationService(appConfig);
}
@Test
void testFeatureFlags() {
assertFalse(configurationService.isFeatureEnabled("non-existent-feature"));
// Assuming test config has these features
assertTrue(configurationService.isFeatureEnabled("enhanced-search"));
assertFalse(configurationService.isFeatureEnabled("new-payment-processor"));
}
@Test
void testOperationalModes() {
assertFalse(configurationService.isMaintenanceMode());
assertFalse(configurationService.isReadOnlyMode());
}
@Test
void testEnvironment() {
assertEquals("test", configurationService.getEnvironment());
}
}
Step 10: Advanced Dhall Features
Dynamic Configuration with Functions
src/main/resources/config/functions.dhall
-- Configuration functions for dynamic generation
let generateDatabaseUrl =
\(environment : Text) ->
\(databaseName : Text) ->
if environment == "production" then
"jdbc:postgresql://prod-db.cluster.local:5432/${databaseName}"
else if environment == "staging" then
"jdbc:postgresql://staging-db.cluster.local:5432/${databaseName}"
else
"jdbc:postgresql://localhost:5432/${databaseName}"
let generateRedisConfig =
\(environment : Text) ->
if environment == "production" then
{ host = "redis-cluster.prod.svc.cluster.local", port = 6379, password = "prod-pass" }
else
{ host = "localhost", port = 6379, password = "" }
let calculatePoolSize =
\(environment : Text) ->
if environment == "production" then
{ minSize = 10, maxSize = 50 }
else if environment == "staging" then
{ minSize = 5, maxSize = 20 }
else
{ minSize = 2, maxSize = 5 }
in { generateDatabaseUrl = generateDatabaseUrl
, generateRedisConfig = generateRedisConfig
, calculatePoolSize = calculatePoolSize
}
Template Configuration
src/main/resources/config/templates/app-template.dhall
-- Configuration template for new applications
let base = ../base.dhall
let functions = ../functions.dhall
let AppTemplate =
\(appName : Text) ->
\(environment : Text) ->
base.default
⫽ { database =
base.default.database
⫽ { url = functions.generateDatabaseUrl environment appName
, pool = functions.calculatePoolSize environment
}
, server = base.default.server ⫽ { contextPath = "/${appName}" }
, environment = environment
}
in AppTemplate
Benefits and Comparison
Advantages of Dhall Configuration
| Feature | Dhall | YAML/JSON | Java Properties |
|---|---|---|---|
| Type Safety | ✅ Compile-time | ❌ Runtime only | ❌ Runtime only |
| Templating | ✅ Native | ❌ Requires tools | ❌ Limited |
| Imports/Reuse | ✅ First-class | ❌ Fragmented | ❌ Basic |
| Validation | ✅ Built-in | ❌ External | ❌ External |
| Functional | ✅ Full support | ❌ None | ❌ None |
| Tooling | ✅ Good | ✅ Excellent | ✅ Excellent |
Performance Characteristics
- Startup Time: Slightly slower due to Dhall evaluation
- Runtime Performance: Same as traditional configuration
- Memory Usage: Minimal overhead
- Build Time: Additional compilation step for Dhall files
Conclusion
Dhall configuration in Java provides:
- Type Safety: Compile-time validation of configuration structure
- Reusability: Functions, imports, and templates for DRY configuration
- Maintainability: Self-documenting configuration with rich types
- Safety: No arbitrary code execution, pure functional evaluation
- Integration: Seamless integration with Spring Boot and other frameworks
By implementing Dhall configuration, you can create robust, maintainable, and type-safe configuration management that scales with your application complexity while reducing configuration-related bugs and issues.