Spring BootCentralized Configuration
Spring Boot

Centralized Configuration

Centralised configuration manages all service properties from a single source of truth, eliminating configuration drift across environments and instances. Spring Cloud Config, Kubernetes ConfigMaps, and environment variables are the three primary strategies. This entry covers environment-specific profiles, configuration precedence, @ConfigurationProperties, feature flags, secrets management, and configuration validation.

Configuration Precedence and Property Sources

Spring Boot evaluates property sources in a fixed precedence order — higher in the list overrides lower. Understanding precedence is essential for predictable behaviour across environments. Environment variables beat application.yml; config server beats local files; command-line arguments beat everything.
yaml
# ── Spring Boot property source precedence (highest to lowest) ────────
# 1.  Command-line arguments          --server.port=9090
# 2.  SPRING_APPLICATION_JSON env var {"server.port":9090}
# 3.  Servlet context init params
# 4.  JNDI attributes
# 5.  Java System properties          -Dserver.port=9090
# 6.  OS environment variables        SERVER_PORT=9090
# 7.  Config server (remote)          application.yml in Git
# 8.  Profile-specific files          application-prod.yml
# 9.  Application properties          application.yml
# 10. @PropertySource annotations
# 11. Default properties

# ── Relaxed binding — all equivalent ────────────────────────────────
# spring.datasource.url=...       (kebab-case — recommended)
# spring.datasource.url=...       (camelCase)
# SPRING_DATASOURCE_URL=...       (SCREAMING_SNAKE — env var)
# spring.datasource.url=...       (dot notation)

# ── Profile-specific files ────────────────────────────────────────────
# application.yml           → always loaded
# application-dev.yml       → loaded when profile=dev
# application-prod.yml      → loaded when profile=prod
# application-test.yml      → loaded when profile=test
#
# Profile-specific values override application.yml values.
# Multiple profiles: SPRING_PROFILES_ACTIVE=prod,eu-west

# ── Standard profile structure ────────────────────────────────────────
# application.yml (base — shared by all environments)
spring:
  application:
    name: order-service
  datasource:
    driver-class-name: org.postgresql.Driver
  jpa:
    open-in-view: false
    hibernate:
      ddl-auto: validate

app:
  pagination:
    default-page-size: 20
    max-page-size: 100

---
# application-dev.yml
spring:
  datasource:
    url: jdbc:postgresql://localhost:5432/orders_dev
    username: dev
    password: dev
  jpa:
    show-sql: true

---
# application-prod.yml
spring:
  datasource:
    url: ${DATABASE_URL}
    username: ${DATABASE_USER}
    password: ${DATABASE_PASSWORD}
  jpa:
    show-sql: false

@ConfigurationProperties

@ConfigurationProperties binds a group of related properties to a typed, validated Java object. It is the preferred way to read configuration — it documents the structure explicitly, supports IDE autocompletion, and validates values at startup. Group related properties by prefix rather than injecting them individually with @Value.
Java
// ── Typed configuration properties ────────────────────────────────────
@ConfigurationProperties(prefix = "app.payments")
@Validated
@Getter @Setter
public class PaymentProperties {

    // ── Gateway settings ──────────────────────────────────────────────
    @NotBlank
    private String gatewayUrl;

    @NotBlank
    private String apiKey;

    @DurationMin(seconds = 1)
    @DurationMax(seconds = 60)
    private Duration timeout = Duration.ofSeconds(30);

    private boolean sandboxMode = false;

    // ── Retry configuration ───────────────────────────────────────────
    @Valid
    @NotNull
    private Retry retry = new Retry();

    @Getter @Setter
    public static class Retry {
        @Min(0) @Max(10)
        private int     maxAttempts   = 3;
        private Duration initialDelay = Duration.ofMillis(100);
        private double  multiplier    = 2.0;
    }

    // ── Supported currencies ──────────────────────────────────────────
    @NotEmpty
    private List<String> supportedCurrencies =
        List.of("USD", "EUR", "GBP");
}

// ── Register as @Bean (Spring Boot 2.2+) ─────────────────────────────
@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

// In any @Configuration class:
@EnableConfigurationProperties(PaymentProperties.class)

// ── application.yml ───────────────────────────────────────────────────
app:
  payments:
    gateway-url: https://payments.example.com/v2
    api-key: ${PAYMENT_API_KEY}
    timeout: 15s
    sandbox-mode: false
    retry:
      max-attempts: 3
      initial-delay: 200ms
      multiplier: 2.0
    supported-currencies:
      - USD
      - EUR
      - GBP

// ── Use in service ────────────────────────────────────────────────────
@Service
@RequiredArgsConstructor
public class PaymentService {

    private final PaymentProperties config;

    public PaymentResponse charge(BigDecimal amount, String currency) {
        if (!config.getSupportedCurrencies().contains(currency)) {
            throw new UnsupportedCurrencyException(currency);
        }
        // Use config.getGatewayUrl(), config.getTimeout(), etc.
    }
}

Feature Flags

Feature flags decouple deployment from release. A feature is deployed but disabled — turned on when ready without a new deployment. Implement feature flags as @ConfigurationProperties beans combined with @RefreshScope so they update at runtime from the config server without restart.
Java
// ── Feature flag configuration ────────────────────────────────────────
@ConfigurationProperties(prefix = "app.features")
@RefreshScope
@Getter @Setter
public class FeatureFlags {

    // Individual flags
    private boolean newCheckoutFlow     = false;
    private boolean recommendationEngine = false;
    private boolean darkMode            = false;

    // Percentage rollout (0-100)
    private int     newCheckoutRollout  = 0;

    // Allowlist for early access
    private Set<String> betaUsers       = new HashSet<>();
}

// ── Feature flag service ───────────────────────────────────────────────
@Service
@RequiredArgsConstructor
@Slf4j
public class FeatureFlagService {

    private final FeatureFlags flags;

    public boolean isEnabled(String flagName, Long userId) {
        return switch (flagName) {
            case "newCheckout" -> isNewCheckoutEnabled(userId);
            case "recommendations" -> flags.isRecommendationEngine();
            default -> {
                log.warn("Unknown feature flag: {}", flagName);
                yield false;
            }
        };
    }

    private boolean isNewCheckoutEnabled(Long userId) {
        // Full feature flag — all users
        if (flags.isNewCheckoutFlow()) return true;

        // Beta allowlist
        if (flags.getBetaUsers()
                .contains(String.valueOf(userId))) return true;

        // Percentage rollout — deterministic per user
        int rollout = flags.getNewCheckoutRollout();
        if (rollout <= 0) return false;
        return Math.abs(userId.hashCode() % 100) < rollout;
    }
}

// ── Controller using feature flag ─────────────────────────────────────
@RestController
@RequestMapping("/api/v1/checkout")
@RequiredArgsConstructor
public class CheckoutController {

    private final FeatureFlagService flags;
    private final CheckoutService    checkoutService;

    @PostMapping
    public ResponseEntity<CheckoutResponse> checkout(
            @RequestBody @Valid CheckoutRequest request,
            @AuthenticationPrincipal AppUser principal) {

        if (flags.isEnabled("newCheckout", principal.getId())) {
            return ResponseEntity.ok(
                checkoutService.processNew(request));
        }
        return ResponseEntity.ok(
            checkoutService.processLegacy(request));
    }
}

# ── Toggle via config server (no restart) ─────────────────────────────
# order-service.yml in Git repo:
app:
  features:
    new-checkout-flow: true        # enable for all users
    new-checkout-rollout: 25       # or 25% rollout
    beta-users:
      - "1001"
      - "1002"

Kubernetes ConfigMap and Secret Integration

When running in Kubernetes, inject configuration from ConfigMaps and Secrets as environment variables or mounted volumes. Spring Boot reads them automatically. Spring Cloud Kubernetes provides native ConfigMap watching to reload @RefreshScope beans when a ConfigMap changes without any webhook.
XML
<!-- pom.xml — Spring Cloud Kubernetes config watcher -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-kubernetes-client-config</artifactId>
</dependency>

# ── Kubernetes ConfigMap ──────────────────────────────────────────────
apiVersion: v1
kind: ConfigMap
metadata:
  name: order-service-config
  namespace: production
data:
  application.yml: |
    app:
      features:
        new-checkout-flow: false
        new-checkout-rollout: 10
      pagination:
        default-page-size: 20
      payments:
        gateway-url: https://payments.prod.example.com
        timeout: 15s

---
# ── Kubernetes Secret (for sensitive values) ──────────────────────────
apiVersion: v1
kind: Secret
metadata:
  name: order-service-secrets
  namespace: production
type: Opaque
stringData:
  DATABASE_URL: jdbc:postgresql://db:5432/orders
  DATABASE_PASSWORD: supersecret
  PAYMENT_API_KEY: pk_live_abc123

---
# ── Deployment — mount ConfigMap and Secret ───────────────────────────
apiVersion: apps/v1
kind: Deployment
metadata:
  name: order-service
spec:
  template:
    spec:
      containers:
        - name: order-service
          envFrom:
            - secretRef:
                name: order-service-secrets
          volumeMounts:
            - name: config-volume
              mountPath: /config
      volumes:
        - name: config-volume
          configMap:
            name: order-service-config

# ── application.yml — load mounted ConfigMap ──────────────────────────
spring:
  config:
    import:
      - "configtree:/config/"     # reads key=file, value=contents

# ── Spring Cloud Kubernetes — auto-reload on ConfigMap change ─────────
spring:
  cloud:
    kubernetes:
      config:
        enabled: true
        name: order-service-config
        namespace: production
      reload:
        enabled: true
        mode: polling          # or event (requires RBAC)
        period: 15000          # check every 15 seconds

Configuration Validation

Validate @ConfigurationProperties at startup using Bean Validation annotations and @Validated. Invalid configuration fails fast — the application refuses to start with a clear error message rather than failing at runtime during the first use of the misconfigured property.
Java
// ── Validated configuration ───────────────────────────────────────────
@ConfigurationProperties(prefix = "app")
@Validated
@Getter @Setter
public class AppProperties {

    @Valid
    @NotNull
    private Database database = new Database();

    @Valid
    @NotNull
    private Security security = new Security();

    @Valid
    @NotNull
    private Messaging messaging = new Messaging();

    @Getter @Setter
    public static class Database {
        @NotBlank(message = "Database URL must be configured")
        private String url;

        @Min(value = 1, message = "Pool size must be at least 1")
        @Max(value = 100, message = "Pool size must not exceed 100")
        private int poolSize = 10;

        @DurationMin(millis = 100)
        private Duration connectionTimeout =
            Duration.ofSeconds(30);
    }

    @Getter @Setter
    public static class Security {
        @NotBlank
        @Size(min = 32,
              message = "JWT secret must be at least 32 characters")
        private String jwtSecret;

        @DurationMin(minutes = 1)
        @DurationMax(hours = 24)
        private Duration accessTokenExpiry =
            Duration.ofMinutes(15);
    }

    @Getter @Setter
    public static class Messaging {
        @NotBlank
        private String brokerUrl;

        @Min(1) @Max(20)
        private int concurrency = 3;
    }
}

// ── Custom validator for cross-field constraints ───────────────────────
@Component
public class AppPropertiesValidator
        implements ApplicationListener<ApplicationStartedEvent> {

    private final AppProperties properties;

    public AppPropertiesValidator(AppProperties properties) {
        this.properties = properties;
    }

    @Override
    public void onApplicationEvent(
            ApplicationStartedEvent event) {
        Security sec = properties.getSecurity();
        Duration access  = sec.getAccessTokenExpiry();

        // Business rule: access token < 1 hour in production
        String profile = System.getenv(
            "SPRING_PROFILES_ACTIVE");
        if ("prod".equals(profile) &&
                access.toHours() >= 1) {
            throw new IllegalStateException(
                "Access token expiry must be < 1 hour in production");
        }
    }
}