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.