Article
In modern application development, managing configuration complexity across multiple environments, services, and teams is a significant challenge. CUE (Configure, Unify, Execute) is a powerful language for defining, validating, and managing configuration data. When combined with Java applications, CUE provides type-safe configuration management, validation, and template generation capabilities that go far beyond traditional configuration formats. This article explores how to integrate CUE with Java applications for robust configuration management.
What is CUE Language?
CUE is an open-source data validation language and inference engine with roots in Go. It combines:
- Schema Definition: Define types and constraints for your data
- Data Validation: Validate configuration against schemas
- Template Generation: Generate configuration for different environments
- Data Unification: Merge multiple configuration sources
Key Benefits for Java Applications
- Type-Safe Configuration: Catch configuration errors at build time
- Multi-Environment Management: Manage dev, staging, prod configs consistently
- Configuration Validation: Ensure configuration meets business rules
- Template Reuse: Avoid configuration duplication
- IDE Support: Get autocomplete and validation in editors
Setting Up CUE with Java
1. CUE Installation
First, install CUE on your system:
# On macOS brew install cue-lang/tap/cue # On Linux wget https://github.com/cue-lang/cue/releases/download/v0.6.0/cue_v0.6.0_linux_amd64.tar.gz tar xzf cue_v0.6.0_linux_amd64.tar.gz sudo mv cue /usr/local/bin/ # Verify installation cue version
2. Java Dependencies
Add CUE Java integration to your pom.xml:
<properties>
<cue.java.version>0.6.0</cue.java.version>
<jackson.version>2.15.2</jackson.version>
</properties>
<dependencies>
<!-- CUE Java binding -->
<dependency>
<groupId>org.cuelang</groupId>
<artifactId>cue-java</artifactId>
<version>${cue.java.version}</version>
</dependency>
<!-- JSON processing -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</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>
<!-- For Spring Boot integration -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>3.1.0</version>
<optional>true</optional>
</dependency>
</dependencies>
CUE Configuration Examples
1. Basic Application Configuration Schema
Create schemas/app.cue:
package app
// Application configuration schema
#AppConfig: {
// Server configuration
server: {
port: int & >=1000 & <=9999
host: string | *"localhost"
// SSL configuration
ssl?: {
enabled: bool | *false
keyPath: string
certPath: string
}
}
// Database configuration
database: {
url: string
username: string
password: string
pool: {
min: int & >=1 & <=20 | *5
max: int & >=5 & <=100 | *20
timeout: int & >=5000 | *30000
}
}
// Feature flags
features: {
caching: bool | *true
metrics: bool | *false
tracing: bool | *false
}
// Logging configuration
logging: {
level: "DEBUG" | "INFO" | "WARN" | "ERROR" | *"INFO"
format: "JSON" | "TEXT" | *"TEXT"
file?: {
path: string
maxSize: string | *"100MB"
maxBackups: int & >=1 | *10
}
}
}
// Environment-specific defaults
#Development: #AppConfig & {
server: host: "localhost"
features: metrics: true
logging: level: "DEBUG"
}
#Production: #AppConfig & {
server: host: "0.0.0.0"
features: {
caching: true
metrics: true
tracing: true
}
logging: {
level: "WARN"
format: "JSON"
file: path: "/var/log/app.log"
}
}
2. Multi-Service Configuration
Create schemas/microservices.cue:
package microservices
// Common service configuration
#ServiceConfig: {
name: string
version: string
environment: "dev" | "staging" | "production"
// Kubernetes resources
resources: {
requests: {
cpu: string | *"100m"
memory: string | *"128Mi"
}
limits: {
cpu: string | *"500m"
memory: string | *"512Mi"
}
}
// Service discovery
discovery: {
port: int & >=3000 & <=9999
healthCheck: string | *"/actuator/health"
}
}
// Database service configuration
#DatabaseService: #ServiceConfig & {
name: "database-service"
discovery: port: 5432
// Database-specific config
database: {
type: "postgres" | "mysql" | "mongodb"
schema?: string
migrations: {
enabled: bool | *true
location: string | *"classpath:db/migration"
}
}
}
// API service configuration
#ApiService: #ServiceConfig & {
name: "api-service"
discovery: port: 8080
// API-specific config
api: {
rateLimit: int & >=0 | *1000
cors: {
allowedOrigins: [...string] | *["*"]
allowedMethods: [...string] | *["GET", "POST", "PUT", "DELETE"]
}
}
// Dependencies
dependencies: {
database: string
cache?: string
messaging?: string
}
}
3. Validation Rules
Create schemas/validation.cue:
package validation
// Email validation
#Email: =~"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$"
// URL validation
#URL: =~"^https?://[a-zA-Z0-9.-]+(?:\\.[a-zA-Z]{2,})+(?:[/?].*)?$"
// User configuration with validation
#UserConfig: {
username: string & =~"^[a-z0-9_-]{3,20}$"
email: #Email
profile: {
firstName: string & =~"^[A-Za-z ]{1,50}$"
lastName: string & =~"^[A-Za-z ]{1,50}$"
age: int & >=0 & <=150
}
preferences?: {
theme: "light" | "dark" | *"light"
language: string | *"en"
notifications: {
email: bool | *true
push: bool | *false
}
}
}
// Business rules validation
#OrderConfig: {
orderId: string & =~"^ORD-[A-Z0-9]{8}$"
customer: #UserConfig
items: [...{
sku: string
quantity: int & >0
price: number & >0.0
}]
total: number & >0.0
// Business rule: total must equal sum of item prices
let itemTotal = sum([for item in items { item.quantity * item.price }])
total: itemTotal
}
Java Integration
1. CUE Configuration Loader
@Component
@Slf4j
public class CueConfigurationLoader {
private final ObjectMapper objectMapper;
private final ProcessBuilder processBuilder;
public CueConfigurationLoader() {
this.objectMapper = new ObjectMapper();
this.processBuilder = new ProcessBuilder();
}
public <T> T loadConfiguration(String cueFile, String expression, Class<T> configType) {
try {
// Execute CUE command to export configuration as JSON
Process process = processBuilder.command(
"cue", "export", cueFile, "-e", expression, "--out", "json"
).start();
// Read the JSON output
String jsonOutput = readProcessOutput(process);
int exitCode = process.waitFor();
if (exitCode != 0) {
String errorOutput = readProcessError(process);
throw new RuntimeException("CUE evaluation failed: " + errorOutput);
}
// Parse JSON into Java object
return objectMapper.readValue(jsonOutput, configType);
} catch (IOException | InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("Failed to load CUE configuration", e);
}
}
public void validateConfiguration(String cueFile, String expression) {
try {
Process process = processBuilder.command(
"cue", "vet", cueFile, "-e", expression
).start();
int exitCode = process.waitFor();
if (exitCode != 0) {
String errorOutput = readProcessError(process);
throw new ConfigurationValidationException(
"Configuration validation failed: " + errorOutput);
}
log.info("Configuration validation passed for: {}", expression);
} catch (IOException | InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("Configuration validation failed", e);
}
}
private String readProcessOutput(Process process) throws IOException {
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream()))) {
return reader.lines().collect(Collectors.joining("\n"));
}
}
private String readProcessError(Process process) throws IOException {
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getErrorStream()))) {
return reader.lines().collect(Collectors.joining("\n"));
}
}
}
class ConfigurationValidationException extends RuntimeException {
public ConfigurationValidationException(String message) {
super(message);
}
}
2. Configuration Data Classes
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class AppConfig {
private ServerConfig server;
private DatabaseConfig database;
private FeaturesConfig features;
private LoggingConfig logging;
@Data
public static class ServerConfig {
private int port;
private String host;
private SslConfig ssl;
@Data
public static class SslConfig {
private boolean enabled;
private String keyPath;
private String certPath;
}
}
@Data
public static class DatabaseConfig {
private String url;
private String username;
private String password;
private PoolConfig pool;
@Data
public static class PoolConfig {
private int min;
private int max;
private int timeout;
}
}
@Data
public static class FeaturesConfig {
private boolean caching;
private boolean metrics;
private boolean tracing;
}
@Data
public static class LoggingConfig {
private String level;
private String format;
private FileConfig file;
@Data
public static class FileConfig {
private String path;
private String maxSize;
private int maxBackups;
}
}
}
@Data
public class UserConfig {
private String username;
private String email;
private ProfileConfig profile;
private PreferencesConfig preferences;
@Data
public static class ProfileConfig {
private String firstName;
private String lastName;
private int age;
}
@Data
public static class PreferencesConfig {
private String theme;
private String language;
private NotificationsConfig notifications;
@Data
public static class NotificationsConfig {
private boolean email;
private boolean push;
}
}
}
3. Spring Boot Integration
@Configuration
@Slf4j
public class CueConfiguration {
@Value("${app.config.cue-file:config/app.cue}")
private String cueConfigFile;
@Value("${app.config.environment:development}")
private String environment;
@Bean
public CueConfigurationLoader cueConfigurationLoader() {
return new CueConfigurationLoader();
}
@Bean
@Primary
public AppConfig appConfig(CueConfigurationLoader loader) {
log.info("Loading CUE configuration for environment: {}", environment);
// Validate configuration first
loader.validateConfiguration(cueConfigFile, getEnvironmentExpression());
// Load configuration
AppConfig config = loader.loadConfiguration(
cueConfigFile,
getEnvironmentExpression(),
AppConfig.class
);
log.info("Successfully loaded CUE configuration: {}", config);
return config;
}
@Bean
public UserConfig userConfigSchema(CueConfigurationLoader loader) {
return loader.loadConfiguration(
"schemas/validation.cue",
"validation.#UserConfig",
UserConfig.class
);
}
private String getEnvironmentExpression() {
return switch (environment.toLowerCase()) {
case "production" -> "app.#Production";
case "staging" -> "app.#AppConfig"; // Use base schema
default -> "app.#Development";
};
}
}
// Property source for CUE configuration
@Component
@Slf4j
public class CuePropertySource extends PropertySource<AppConfig> {
private final AppConfig appConfig;
public CuePropertySource(AppConfig appConfig) {
super("cueConfiguration");
this.appConfig = appConfig;
}
@Override
public Object getProperty(String name) {
// Map configuration properties to Spring environment
return switch (name) {
case "server.port" -> appConfig.getServer().getPort();
case "server.host" -> appConfig.getServer().getHost();
case "database.url" -> appConfig.getDatabase().getUrl();
case "database.username" -> appConfig.getDatabase().getUsername();
case "database.password" -> appConfig.getDatabase().getPassword();
case "logging.level" -> appConfig.getLogging().getLevel();
default -> null;
};
}
}
4. Advanced CUE Service
@Service
@Slf4j
public class CueConfigurationService {
private final CueConfigurationLoader loader;
private final ObjectMapper objectMapper;
public CueConfigurationService(CueConfigurationLoader loader) {
this.loader = loader;
this.objectMapper = new ObjectMapper()
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
}
public <T> T generateConfiguration(String templateFile,
Map<String, Object> parameters,
Class<T> configType) {
try {
// Create temporary CUE file with parameters
Path tempFile = createParameterizedCueFile(templateFile, parameters);
// Generate configuration
T config = loader.loadConfiguration(
tempFile.toString(),
"config",
configType
);
// Cleanup
Files.delete(tempFile);
return config;
} catch (IOException e) {
throw new RuntimeException("Failed to generate configuration", e);
}
}
public void validateUserConfiguration(UserConfig userConfig) {
try {
// Convert to JSON for CUE validation
String userJson = objectMapper.writeValueAsString(userConfig);
// Create temporary file for validation
Path tempFile = Files.createTempFile("user-config", ".json");
Files.write(tempFile, userJson.getBytes());
// Validate against schema
loader.validateConfiguration("schemas/validation.cue",
"validation.#UserConfig & \(tempFile)");
// Cleanup
Files.delete(tempFile);
} catch (IOException e) {
throw new RuntimeException("User configuration validation failed", e);
}
}
public Map<String, Object> mergeConfigurations(String baseConfig,
String overrideConfig) {
try {
// Create merged CUE configuration
String mergedCue = String.format("""
import "list"
base: %s
override: %s
config: base & override
""", baseConfig, overrideConfig);
Path tempFile = Files.createTempFile("merged-config", ".cue");
Files.write(tempFile, mergedCue.getBytes());
@SuppressWarnings("unchecked")
Map<String, Object> result = loader.loadConfiguration(
tempFile.toString(),
"config",
Map.class
);
Files.delete(tempFile);
return result;
} catch (IOException e) {
throw new RuntimeException("Configuration merge failed", e);
}
}
private Path createParameterizedCueFile(String templateFile,
Map<String, Object> parameters) throws IOException {
String templateContent = Files.readString(Path.of(templateFile));
// Replace placeholders with actual values
for (Map.Entry<String, Object> entry : parameters.entrySet()) {
String placeholder = "\\$\\{" + entry.getKey() + "\\}";
templateContent = templateContent.replaceAll(placeholder,
objectMapper.writeValueAsString(entry.getValue()));
}
Path tempFile = Files.createTempFile("parameterized", ".cue");
Files.write(tempFile, templateContent.getBytes());
return tempFile;
}
}
5. Template-Based Configuration Generation
Create templates/k8s-deployment.cue:
package k8s
import "list"
#DeploymentTemplate: {
apiVersion: "apps/v1"
kind: "Deployment"
metadata: {
name: string
namespace: string | *"default"
labels: [string]: string
}
spec: {
replicas: int & >=1 | *3
selector: matchLabels: {
"app": metadata.name
}
template: {
metadata: labels: {
"app": metadata.name
"version": string
}
spec: {
containers: [{
name: metadata.name
image: string
ports: [{
containerPort: int
protocol: "TCP" | *"TCP"
}]
env: [...{
name: string
value: string
}]
resources: {
requests: {
cpu: string
memory: string
}
limits: {
cpu: string
memory: string
}
}
livenessProbe: {
httpGet: {
path: string
port: int
}
initialDelaySeconds: int | *30
periodSeconds: int | *10
}
}]
}
}
}
}
// Generate deployment for a service
deployment: #DeploymentTemplate & {
metadata: {
name: "user-service"
namespace: "production"
labels: {
"app": "user-service"
"environment": "production"
}
}
spec: {
replicas: 3
template: {
metadata: labels: {
"app": "user-service"
"version": "1.2.3"
}
spec: containers: [{
name: "user-service"
image: "myregistry/user-service:1.2.3"
ports: [{
containerPort: 8080
}]
env: [
{
name: "DATABASE_URL"
value: "postgresql://localhost:5432/users"
},
{
name: "LOG_LEVEL"
value: "INFO"
}
]
resources: {
requests: {
cpu: "100m"
memory: "256Mi"
}
limits: {
cpu: "500m"
memory: "512Mi"
}
}
livenessProbe: {
httpGet: {
path: "/actuator/health"
port: 8080
}
initialDelaySeconds: 45
}
}]
}
}
}
6. Kubernetes Configuration Generator
@Service
@Slf4j
public class KubernetesConfigGenerator {
private final CueConfigurationService cueService;
private final ObjectMapper yamlMapper;
public KubernetesConfigGenerator(CueConfigurationService cueService) {
this.cueService = cueService;
this.yamlMapper = new ObjectMapper(new YAMLFactory());
}
public String generateDeployment(String serviceName, String version,
String environment, int replicas) {
Map<String, Object> parameters = Map.of(
"serviceName", serviceName,
"version", version,
"environment", environment,
"replicas", replicas,
"image", String.format("myregistry/%s:%s", serviceName, version)
);
@SuppressWarnings("unchecked")
Map<String, Object> deployment = cueService.generateConfiguration(
"templates/k8s-deployment.cue",
parameters,
Map.class
);
try {
return yamlMapper.writeValueAsString(deployment);
} catch (IOException e) {
throw new RuntimeException("Failed to generate YAML", e);
}
}
public void generateAllEnvironments(String serviceName, String version) {
List<String> environments = List.of("development", "staging", "production");
for (String env : environments) {
String deploymentYaml = generateDeployment(
serviceName, version, env, getReplicaCount(env));
String filename = String.format("k8s/%s-%s-deployment.yaml", serviceName, env);
try {
Files.writeString(Path.of(filename), deploymentYaml);
log.info("Generated deployment manifest: {}", filename);
} catch (IOException e) {
log.error("Failed to write deployment file: {}", filename, e);
}
}
}
private int getReplicaCount(String environment) {
return switch (environment) {
case "production" -> 5;
case "staging" -> 2;
default -> 1;
};
}
}
Build and Usage
1. Maven Build Integration
Create src/main/cue/schemas/app.cue and add to pom.xml:
<build> <plugins> <plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>exec-maven-plugin</artifactId> <version>3.1.0</version> <executions> <execution> <id>validate-cue-config</id> <phase>validate</phase> <goals> <goal>exec</goal> </goals> <configuration> <executable>cue</executable> <arguments> <argument>vet</argument> <argument>src/main/cue/schemas/app.cue</argument> <argument>app.#AppConfig</argument> </arguments> </configuration> </execution> </executions> </plugin> </plugins> </build>
2. Application Configuration
application.yml:
app:
config:
cue-file: "src/main/cue/schemas/app.cue"
environment: ${APP_ENVIRONMENT:development}
spring:
profiles:
active: ${APP_ENVIRONMENT:development}
Benefits and Use Cases
Benefits:
- Type Safety: Catch configuration errors before runtime
- Validation: Complex business rules enforced in configuration
- Templating: Generate multiple environment configurations
- Reuse: Share configuration schemas across projects
- IDE Support: Autocomplete and validation in CUE-aware editors
Use Cases:
- Multi-environment configuration management
- Kubernetes manifest generation
- API specification validation
- Database schema management
- Feature flag configuration
- Microservices configuration sharing
Conclusion
Integrating CUE with Java applications provides a powerful approach to configuration management that combines type safety, validation, and templating capabilities. By leveraging CUE's strong typing and validation features, Java applications can:
- Eliminate configuration errors through compile-time validation
- Manage complex multi-environment setups with reusable templates
- Generate deployment artifacts like Kubernetes manifests
- Ensure consistency across microservices configurations
- Provide better developer experience with schema-aware configuration editing
The combination of CUE's powerful configuration language with Java's robust application runtime creates a foundation for reliable, maintainable, and scalable application configuration management.