← Kursa Dön
📄 Text · 15 min

Configuration Best Practices

Giriş

Bir uygulamanın konfigürasyonu, onun DNA'sıdır. Veritabanı adresi, API key'leri, cache süreleri, feature flag'leri — bunlar uygulamanın nasıl davranacağını belirler. Yanlış bir konfigürasyon, uygulamanızı production'da çökertebilir. Doğru bir konfigürasyon stratejisi ise uygulamanızı farklı ortamlarda güvenle çalıştırmanızı, secret'ları korumanızı ve değişiklikleri restart'sız uygulamanızı sağlar.

Bunu bir arabanın kontrol paneli gibi düşünün: hız, devir, yakıt, motor sıcaklığı — her şey doğru göstergelerde, doğru zamanda, doğru değerlerle olmalı. Yanlış kablolama yaparsanız hız göstergesi sıcaklığı, yakıt göstergesi devri gösterir — kaos.

Bu derste Spring Boot'un konfigürasyon sistemini derinlemesine öğrenecek, type-safe configuration, profil yönetimi, secret management ve production-ready best practice'leri uygulayacağız.

Konfigürasyon Hiyerarşisi (Öncelik Sırası)

Spring Boot, konfigürasyon değerlerini birden fazla kaynaktan okur. Bu kaynaklar belirli bir öncelik sırasına sahiptir — üstteki alttakini ezer (override eder):

1. Command line arguments          (--server.port=9090)
2. SPRING_APPLICATION_JSON         (inline JSON)
3. ServletConfig / ServletContext  (web parametreleri)
4. JNDI attributes                 (java:comp/env)
5. Java System properties          (-Dserver.port=9090)
6. OS environment variables        (SERVER_PORT=9090)
7. RandomValuePropertySource       (random.int, random.uuid)
8. application-{profile}.yml       (profil-specific)
9. application.yml                 (varsayılan)
10. @PropertySource                (özel dosyalar)
11. SpringApplication defaults     (kod içi varsayılan)

Bu hiyerarşi neden önemli? Çünkü aynı property'yi farklı yerlerde tanımlayabilirsiniz ve Spring Boot hangisinin kazanacağını bu sıraya göre belirler:

# application.yml — en düşük öncelik
server:
  port: 8080
# Environment variable — daha yüksek öncelik
export SERVER_PORT=9090
# Command line — EN yüksek öncelik
java -jar app.jar --server.port=9999

Bu durumda uygulama 9999 portunda çalışır. Bu mekanizma, aynı JAR dosyasını farklı ortamlarda farklı konfigürasyonlarla çalıştırmanızı sağlar — Twelve-Factor App'in "Config" prensibinin temelidir.

application.yml vs application.properties

Spring Boot iki format destekler. Her ikisi de aynı işi yapar, ancak YAML daha okunabilir ve hiyerarşik yapıyı daha iyi ifade eder:

# application.properties — düz format
spring.datasource.url=jdbc:postgresql://localhost:5432/mydb
spring.datasource.username=admin
spring.datasource.password=secret
spring.datasource.hikari.maximum-pool-size=20
spring.datasource.hikari.minimum-idle=5
# application.yml — hiyerarşik format
spring:
  datasource:
    url: jdbc:postgresql://localhost:5432/mydb
    username: admin
    password: secret
    hikari:
      maximum-pool-size: 20
      minimum-idle: 5

💡 İpucu: Projelerde YAML tercih edin. Nested yapıları daha temiz ifade eder, liste desteği doğaldır ve daha az tekrar gerektirir. Ancak YAML'ın girintilere (indentation) duyarlı olduğunu unutmayın — bir boşluk hatası tüm konfigürasyonu bozabilir.

Profile (Ortam) Yönetimi

Gerçek dünya uygulamaları farklı ortamlarda çalışır: development, testing, staging, production. Her ortamın farklı konfigürasyona ihtiyacı vardır. Spring Boot'un profil sistemi bunu zarif bir şekilde çözer.

Profil Dosyaları

src/main/resources/
├── application.yml                    # Tüm ortamlar için ortak
├── application-dev.yml                # Development
├── application-test.yml               # Test
├── application-staging.yml            # Staging
└── application-prod.yml               # Production
# application.yml — ortak konfigürasyon
spring:
  application:
    name: order-service
  jpa:
    open-in-view: false

server:
  servlet:
    context-path: /api
# application-dev.yml
spring:
  datasource:
    url: jdbc:h2:mem:devdb
    driver-class-name: org.h2.Driver
  jpa:
    hibernate:
      ddl-auto: create-drop
    show-sql: true
  h2:
    console:
      enabled: true

logging:
  level:
    com.example: DEBUG
    org.hibernate.SQL: DEBUG
# application-prod.yml
spring:
  datasource:
    url: ${DATABASE_URL}
    username: ${DB_USERNAME}
    password: ${DB_PASSWORD}
    hikari:
      maximum-pool-size: 30
      minimum-idle: 10
      connection-timeout: 20000
  jpa:
    hibernate:
      ddl-auto: validate
    show-sql: false

logging:
  level:
    com.example: INFO
    org.hibernate: WARN

server:
  port: ${PORT:8080}

Profil Aktivasyonu

# Environment variable ile
export SPRING_PROFILES_ACTIVE=prod

# Command line ile
java -jar app.jar --spring.profiles.active=prod

# Docker'da
docker run -e SPRING_PROFILES_ACTIVE=prod myapp:latest

# application.yml'da varsayılan profil
spring:
  profiles:
    active: dev

Profil Grupları (Spring Boot 2.4+)

Birden fazla profili gruplamak için:

# application.yml
spring:
  profiles:
    group:
      prod:
        - prod-db
        - prod-cache
        - prod-security
      dev:
        - dev-db
        - dev-cache

Bu sayede SPRING_PROFILES_ACTIVE=prod dediğinizde prod-db, prod-cache ve prod-security profilleri otomatik aktif olur.

⚠️ Dikkat: Production'da ddl-auto: create-drop veya update ASLA kullanmayın. Bu, tabloları silip yeniden oluşturur veya yapısını değiştirir. Production'da validate kullanın — sadece entity ile tablo yapısının eşleştiğini doğrular. Schema değişiklikleri için Flyway veya Liquibase kullanın.

@Value — Basit Değer Enjeksiyonu

Tek bir property değerini bean'e enjekte etmenin en basit yolu:

@Service
public class NotificationService {

    @Value("${app.notification.email.from}")
    private String fromEmail;

    @Value("${app.notification.email.enabled:true}")  // varsayılan değer
    private boolean emailEnabled;

    @Value("${app.notification.max-retry:3}")
    private int maxRetry;

    @Value("${app.notification.recipients}")  // virgülle ayrılmış liste
    private List<String> recipients;

    @Value("#{${app.notification.templates}}")  // SpEL ile Map
    private Map<String, String> templates;
}
app:
  notification:
    email:
      from: noreply@example.com
      enabled: true
    max-retry: 3
    recipients: admin@example.com,ops@example.com
    templates: "{welcome: 'welcome.html', reset: 'reset-password.html'}"

@Value'nun Sınırları

@Value basit senaryolar için uygundur, ancak ciddi dezavantajları vardır:

// ❌ KÖTÜ — @Value ile 10+ property yönetmek
@Service
public class PaymentService {

    @Value("${payment.gateway.url}")
    private String gatewayUrl;

    @Value("${payment.gateway.api-key}")
    private String apiKey;

    @Value("${payment.gateway.secret}")
    private String secret;

    @Value("${payment.gateway.timeout:5000}")
    private int timeout;

    @Value("${payment.gateway.retry:3}")
    private int retry;

    @Value("${payment.gateway.sandbox:false}")
    private boolean sandbox;

    // 6 tane @Value — karışık, hata yapma riski yüksek
    // Typo yaparsan runtime'da patlıyor!
}

Sorunlar:

  • Property adında typo yaparsanız derleme zamanında hata almaz, runtime'da patlarsınız

  • Tip dönüşüm hatalarını önceden yakalayamazsınız

  • Validasyon yapılamaz

  • Refactoring zor — property adını değiştirince tüm @Value kullanımlarını bulmanız gerekir

  • Test etmesi zor

@ConfigurationProperties — Type-Safe Configuration

@ConfigurationProperties, Spring Boot'un en güçlü konfigürasyon mekanizmasıdır. Property'leri tip-güvenli (type-safe) bir Java nesnesine bağlar:

@ConfigurationProperties(prefix = "app.payment")
@Validated
public record PaymentConfig(
    @NotBlank String gatewayUrl,
    @NotBlank String apiKey,
    @NotBlank String secret,
    @Min(1000) @Max(30000) int timeout,
    @Min(0) @Max(10) int retry,
    boolean sandbox,
    @Valid RateLimitConfig rateLimit
) {
    public record RateLimitConfig(
        @Min(1) int requestsPerSecond,
        @Min(1) int burstCapacity
    ) {}
}
app:
  payment:
    gateway-url: https://api.payment.com/v2
    api-key: ${PAYMENT_API_KEY}
    secret: ${PAYMENT_SECRET}
    timeout: 5000
    retry: 3
    sandbox: false
    rate-limit:
      requests-per-second: 100
      burst-capacity: 200
// Kullanım — temiz, type-safe, IDE auto-complete çalışır
@Service
@RequiredArgsConstructor
public class PaymentService {

    private final PaymentConfig config;

    public PaymentResult charge(PaymentRequest request) {
        HttpClient client = HttpClient.newBuilder()
            .connectTimeout(Duration.ofMillis(config.timeout()))
            .build();

        if (config.sandbox()) {
            log.info("SANDBOX MODE — gerçek ödeme yapılmıyor");
            return PaymentResult.sandboxSuccess();
        }

        // config.gatewayUrl(), config.apiKey() — typo imkansız
        return executePayment(client, request);
    }
}

Aktivasyon

@SpringBootApplication
@ConfigurationPropertiesScan  // Paket altındaki tüm @ConfigurationProperties'i tarar
public class MyApplication {
    public static void main(String[] args) {
        SpringApplication.run(MyApplication.class, args);
    }
}

// Veya tek tek kayıt:
@Configuration
@EnableConfigurationProperties(PaymentConfig.class)
public class AppConfig {}

Class vs Record

Java 17+ kullanıyorsanız record kullanın — daha az boilerplate, otomatik immutable:

// ✅ Record ile — temiz, immutable, otomatik getter/toString/equals
@ConfigurationProperties(prefix = "app.mail")
@Validated
public record MailConfig(
    @NotBlank String host,
    @Min(1) @Max(65535) int port,
    @NotBlank String username,
    @NotBlank String password,
    boolean starttls,
    @Valid PoolConfig pool
) {
    public record PoolConfig(
        @Min(1) int coreSize,
        @Min(1) int maxSize,
        @Min(1000) int keepAlive
    ) {}
}
// Geleneksel class ile — daha fazla boilerplate ama setter ile mutable
@ConfigurationProperties(prefix = "app.mail")
@Validated
@Getter
@Setter
public class MailConfig {

    @NotBlank
    private String host;

    @Min(1) @Max(65535)
    private int port;

    @NotBlank
    private String username;

    @NotBlank
    private String password;

    private boolean starttls = false;

    @Valid
    private PoolConfig pool = new PoolConfig();

    @Getter
    @Setter
    public static class PoolConfig {
        @Min(1) private int coreSize = 5;
        @Min(1) private int maxSize = 10;
        @Min(1000) private int keepAlive = 60000;
    }
}

💡 İpucu: Record kullandığınızda varsayılan değer constructor'dan gelir. Property dosyasında tanımlı olmayan bir değer için record constructor'a default parametre ekleyemezsiniz — bu yüzden YAML'da tüm değerleri belirtmeniz gerekir veya @DefaultValue annotation kullanın:

@ConfigurationProperties(prefix = "app.cache")
public record CacheConfig(
    @DefaultValue("true") boolean enabled,
    @DefaultValue("3600") int ttlSeconds,
    @DefaultValue("1000") int maxSize
) {}

Konfigürasyon Validasyonu

Spring Boot, @Validated annotation'ı ile konfigürasyon değerlerini uygulama başlangıcında doğrular. Yanlış konfigürasyon varsa uygulama başlamaz — fail-fast prensibi:

@ConfigurationProperties(prefix = "app.database")
@Validated
public record DatabaseConfig(
    @NotBlank(message = "Database URL boş olamaz")
    String url,

    @NotBlank
    String username,

    @NotBlank
    String password,

    @Min(value = 1, message = "Pool size en az 1 olmalı")
    @Max(value = 100, message = "Pool size en fazla 100 olabilir")
    int poolSize,

    @DurationMin(seconds = 1)
    @DurationMax(seconds = 60)
    Duration connectionTimeout,

    @Pattern(regexp = "^(postgresql|mysql|h2)$",
             message = "Desteklenen DB türleri: postgresql, mysql, h2")
    String type
) {}

Eğer geçersiz bir değer verilirse uygulama hemen başlamaz:

***************************
APPLICATION FAILED TO START
***************************

Description:
Binding to target app.database failed:

  Property: app.database.url
  Value: (empty)
  Reason: Database URL boş olamaz

  Property: app.database.pool-size
  Value: -5
  Reason: Pool size en az 1 olmalı

Bu davranış kritiktir — yanlış konfigürasyonla çalışan bir uygulama, başlamayan bir uygulamadan çok daha tehlikelidir. Production'da yanlış veritabanına bağlanmak, bağlanamamaktan kötüdür.

Secret Management — Gizli Bilgi Yönetimi

Konfigürasyon yönetiminin en kritik konularından biri secret'ların güvenli yönetimidir. Veritabanı şifreleri, API key'leri, JWT secret'ları — bunlar asla kaynak kodunda veya property dosyalarında açık metin olarak bulunmamalıdır.

Seviye 1: Hardcoded (❌ ASLA)

# ❌ EN KÖTÜ — Git history'de sonsuza dek kalır
spring:
  datasource:
    password: SuperSecret123!
  
app:
  jwt:
    secret: myJwtSecretKey12345
  payment:
    api-key: sk_live_abc123def456

Bu yaklaşımın tehlikeleri:

  • Git repository'ye push edilir, tüm ekip görür

  • Git history'den silmek neredeyse imkansız

  • Repo public olursa tüm secret'lar ifşa olur

  • Ortam ayrımı yapılamaz (dev ve prod aynı secret'ı kullanır)

Seviye 2: Environment Variables (✅ Minimum)

# application-prod.yml
spring:
  datasource:
    url: ${DATABASE_URL}
    username: ${DB_USERNAME}
    password: ${DB_PASSWORD}

app:
  jwt:
    secret: ${JWT_SECRET}
  payment:
    api-key: ${PAYMENT_API_KEY}
# Docker / Kubernetes'de
export DATABASE_URL=jdbc:postgresql://prod-db:5432/mydb
export DB_USERNAME=app_user
export DB_PASSWORD=SuperSecret123!
export JWT_SECRET=myLongRandomSecretKeyForJWT
# docker-compose.yml
services:
  app:
    environment:
      DATABASE_URL: jdbc:postgresql://db:5432/mydb
      DB_PASSWORD: ${DB_PASSWORD}  # .env dosyasından

⚠️ Dikkat: Environment variable'lar process listesinde (ps aux), Docker inspect'te ve crash dump'larda görünebilir. Minimum seviye olarak kabul edilir, ancak hassas ortamlar için yeterli değildir.

Seviye 3: External Secret Store (✅✅ Önerilen)

Production ortamlar için HashiCorp Vault, AWS Secrets Manager veya Azure Key Vault gibi çözümler kullanın:

<!-- HashiCorp Vault entegrasyonu -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-vault-config</artifactId>
</dependency>
# application.yml
spring:
  cloud:
    vault:
      uri: https://vault.example.com
      token: ${VAULT_TOKEN}
      kv:
        backend: secret
        default-context: order-service
      authentication: TOKEN
  config:
    import: optional:vault://
// Vault'taki secret'lar otomatik olarak property olarak erişilebilir
@ConfigurationProperties(prefix = "app.database")
public record DatabaseConfig(
    String url,       // vault: secret/order-service → app.database.url
    String username,  // vault: secret/order-service → app.database.username
    String password   // vault: secret/order-service → app.database.password
) {}

Seviye 4: Kubernetes Secrets + Volume Mount

# Kubernetes Secret
apiVersion: v1
kind: Secret
metadata:
  name: app-secrets
type: Opaque
data:
  db-password: U3VwZXJTZWNyZXQxMjMh  # base64 encoded
  jwt-secret: bXlKd3RTZWNyZXRLZXk=
# Spring Boot — dosyadan secret okuma
spring:
  config:
    import: optional:file:/etc/secrets/
  datasource:
    password: ${db-password}

Relaxed Binding — Esnek Bağlama

Spring Boot, property isimlerinde esnek eşleştirme yapar. Aşağıdaki tüm formatlar aynı property'yi ifade eder:

# Aşağıdakiler HEPSİ aynı property'ye bağlanır:
app:
  payment-gateway:           # kebab-case (YAML önerilen)
    api-key: abc

# application.properties'de:
app.paymentGateway.apiKey    # camelCase
app.payment-gateway.api-key  # kebab-case
app.payment_gateway.api_key  # underscore
APP_PAYMENTGATEWAY_APIKEY    # UPPER_CASE (env variable)
@ConfigurationProperties(prefix = "app.payment-gateway")
public record PaymentGatewayConfig(
    String apiKey  // Java'da camelCase — tüm formatlara bağlanır
) {}

💡 İpucu: YAML dosyalarında kebab-case (api-key), Java kodunda camelCase (apiKey), environment variable'larda UPPER_CASE (API_KEY) kullanın. Spring Boot hepsini otomatik eşleştirir.

Custom Configuration Metadata

IDE auto-complete ve dokümantasyon için konfigürasyon metadata dosyası oluşturun:

// src/main/resources/META-INF/additional-spring-configuration-metadata.json
{
  "properties": [
    {
      "name": "app.payment.gateway-url",
      "type": "java.lang.String",
      "description": "Payment gateway base URL",
      "defaultValue": "https://api.payment.com/v2"
    },
    {
      "name": "app.payment.timeout",
      "type": "java.lang.Integer",
      "description": "Payment request timeout in milliseconds",
      "defaultValue": 5000
    },
    {
      "name": "app.payment.sandbox",
      "type": "java.lang.Boolean",
      "description": "Enable sandbox mode for testing",
      "defaultValue": false
    }
  ]
}

Spring Boot annotation processor ile otomatik metadata üretimi:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-configuration-processor</artifactId>
    <optional>true</optional>
</dependency>

Bu dependency eklendiğinde, @ConfigurationProperties sınıflarınız için metadata otomatik üretilir ve IDE'de auto-complete çalışır.

Conditional Configuration — Koşullu Konfigürasyon

Belirli koşullara göre farklı bean'ler veya konfigürasyonlar aktif edebilirsiniz:

@Configuration
public class CacheConfig {

    // Redis varsa Redis cache kullan
    @Bean
    @ConditionalOnProperty(name = "app.cache.type", havingValue = "redis")
    public CacheManager redisCacheManager(RedisConnectionFactory factory) {
        return RedisCacheManager.builder(factory)
            .cacheDefaults(RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofMinutes(30)))
            .build();
    }

    // Redis yoksa in-memory cache kullan
    @Bean
    @ConditionalOnProperty(name = "app.cache.type",
                          havingValue = "memory",
                          matchIfMissing = true)
    public CacheManager inMemoryCacheManager() {
        CaffeineCacheManager manager = new CaffeineCacheManager();
        manager.setCaffeine(Caffeine.newBuilder()
            .maximumSize(1000)
            .expireAfterWrite(Duration.ofMinutes(10)));
        return manager;
    }
}
# Dev ortamında
app:
  cache:
    type: memory

# Prod ortamında
app:
  cache:
    type: redis

Property Placeholder ve SpEL

Konfigürasyon değerlerinde dinamik ifadeler kullanabilirsiniz:

app:
  name: order-service
  version: 1.0.0
  description: "${app.name} v${app.version}"  # Property placeholder
  
  server:
    port: ${PORT:8080}                          # Varsayılan değer
    host: ${HOSTNAME:localhost}
    base-url: "http://${app.server.host}:${app.server.port}"
    
  feature:
    new-ui: ${FEATURE_NEW_UI:false}
@Service
public class AppInfoService {

    // SpEL (Spring Expression Language) ile
    @Value("#{${app.limits} ?: 100}")
    private int limit;

    @Value("#{systemProperties['user.timezone']}")
    private String timezone;

    @Value("#{T(java.lang.Runtime).getRuntime().availableProcessors()}")
    private int cpuCores;
}

Yaygın Hatalar ve Çözümleri

1. Secret'ları Commit Etmek

# ❌ YANLIŞ
spring:
  datasource:
    password: prod_password_123

# ✅ DOĞRU
spring:
  datasource:
    password: ${DB_PASSWORD}

Çözüm: .gitignore'a .env dosyasını ekleyin. Git hook ile secret pattern'leri kontrol edin:

# .gitignore
.env
*.env
application-local.yml

2. Profil Karışıklığı

# ❌ YANLIŞ — prod profile dosyasında dev ayarları
# application-prod.yml
spring:
  jpa:
    hibernate:
      ddl-auto: update     # ← PROD'da asla!
    show-sql: true          # ← Performans kaybı

# ✅ DOĞRU
# application-prod.yml
spring:
  jpa:
    hibernate:
      ddl-auto: validate
    show-sql: false

3. @Value ile Typo

// ❌ YANLIŞ — typo fark edilmez, runtime'da patlıyor
@Value("${app.paymnet.url}")  // "payment" değil "paymnet"
private String paymentUrl;

// ✅ DOĞRU — @ConfigurationProperties ile derleme zamanında hata
@ConfigurationProperties(prefix = "app.payment")
public record PaymentConfig(String url) {}

4. Validasyon Eksikliği

// ❌ YANLIŞ — pool size -1 olsa bile uygulama başlar, çöker
@Value("${app.db.pool-size}")
private int poolSize;

// ✅ DOĞRU — geçersiz değerde uygulama başlamaz
@ConfigurationProperties(prefix = "app.db")
@Validated
public record DbConfig(
    @Min(1) @Max(100) int poolSize
) {}

5. Tüm Ortamlarda Aynı Konfigürasyon

# ❌ YANLIŞ — dev, staging, prod hepsi aynı
spring:
  datasource:
    url: jdbc:postgresql://localhost:5432/mydb

# ✅ DOĞRU — ortama göre farklı
# application-dev.yml → H2 in-memory
# application-staging.yml → staging DB
# application-prod.yml → ${DATABASE_URL}

Gerçek Dünya Örneği: E-Ticaret Konfigürasyonu

Bir e-ticaret uygulamasının tam konfigürasyon yapısı:

@ConfigurationProperties(prefix = "app")
@Validated
public record AppConfig(
    @Valid PaymentConfig payment,
    @Valid ShippingConfig shipping,
    @Valid NotificationConfig notification,
    @Valid CacheConfig cache,
    @Valid SecurityConfig security
) {
    public record PaymentConfig(
        @NotBlank String gatewayUrl,
        @NotBlank String apiKey,
        @Min(1000) int timeoutMs,
        @Min(0) @Max(5) int maxRetry,
        boolean sandboxMode,
        @Valid List<@NotBlank String> supportedMethods
    ) {}

    public record ShippingConfig(
        @NotBlank String providerUrl,
        @Positive BigDecimal freeShippingThreshold,
        @Min(1) @Max(30) int estimatedDaysDefault,
        Map<String, BigDecimal> zonePrices
    ) {}

    public record NotificationConfig(
        @Valid EmailConfig email,
        @Valid SmsConfig sms,
        boolean enabled
    ) {
        public record EmailConfig(
            @NotBlank @Email String from,
            @NotBlank String host,
            @Min(1) @Max(65535) int port,
            boolean starttls
        ) {}

        public record SmsConfig(
            @NotBlank String provider,
            @NotBlank String apiKey,
            @NotBlank String fromNumber
        ) {}
    }

    public record CacheConfig(
        boolean enabled,
        @Min(60) int ttlSeconds,
        @Min(100) int maxEntries,
        String type
    ) {}

    public record SecurityConfig(
        @NotBlank String jwtSecret,
        @Min(300) int jwtExpirationSeconds,
        @Min(1) int maxLoginAttempts,
        @Min(60) int lockoutDurationSeconds
    ) {}
}
# application.yml — ortak
app:
  cache:
    enabled: true
    ttl-seconds: 3600
    max-entries: 5000
    type: memory
  notification:
    enabled: true
  security:
    max-login-attempts: 5
    lockout-duration-seconds: 900

# application-prod.yml
app:
  payment:
    gateway-url: https://api.stripe.com/v1
    api-key: ${STRIPE_API_KEY}
    timeout-ms: 10000
    max-retry: 3
    sandbox-mode: false
    supported-methods:
      - CREDIT_CARD
      - DEBIT_CARD
      - BANK_TRANSFER
  shipping:
    provider-url: https://api.shipping.com
    free-shipping-threshold: 150.00
    estimated-days-default: 3
    zone-prices:
      local: 9.99
      national: 19.99
      international: 49.99
  notification:
    email:
      from: orders@myshop.com
      host: ${SMTP_HOST}
      port: 587
      starttls: true
    sms:
      provider: twilio
      api-key: ${TWILIO_API_KEY}
      from-number: ${TWILIO_FROM_NUMBER}
  cache:
    type: redis
    ttl-seconds: 1800
  security:
    jwt-secret: ${JWT_SECRET}
    jwt-expiration-seconds: 3600
// Kullanım — her yerde temiz, type-safe erişim
@Service
@RequiredArgsConstructor
public class OrderService {

    private final AppConfig config;

    public ShippingCost calculateShipping(BigDecimal orderTotal, String zone) {
        if (orderTotal.compareTo(
                config.shipping().freeShippingThreshold()) >= 0) {
            return ShippingCost.free();
        }

        BigDecimal price = config.shipping().zonePrices()
            .getOrDefault(zone, BigDecimal.valueOf(19.99));

        return new ShippingCost(price,
            config.shipping().estimatedDaysDefault());
    }

    public boolean isPaymentMethodSupported(String method) {
        return config.payment().supportedMethods().contains(method);
    }
}

Best Practices Kontrol Listesi

  1. @ConfigurationProperties kullanın@Value yerine type-safe binding tercih edin

  2. Validasyon ekleyin@Validated ile geçersiz konfigürasyonda fail-fast

  3. Secret'ları dışarıda tutun — En az environment variable, ideal olarak Vault

  4. Profil kullanın — Her ortam için ayrı konfigürasyon dosyası

  5. Varsayılan değerler belirleyin${PROP:default} formatı ile

  6. YAML tercih edin — Hiyerarşik yapıyı daha iyi ifade eder

  7. Metadata oluşturunspring-boot-configuration-processor ile IDE desteği

  8. Konfigürasyonu test edin@SpringBootTest ile konfigürasyon validasyonunu test edin

  9. Production'da ddl-auto: validate — Asla update veya create-drop

  10. .gitignore.env, application-local.yml dosyalarını commit etmeyin

Konfigürasyon Test Etme

@SpringBootTest
@ActiveProfiles("test")
class AppConfigTest {

    @Autowired
    private AppConfig config;

    @Test
    void paymentConfigShouldBeValid() {
        assertThat(config.payment().gatewayUrl()).isNotBlank();
        assertThat(config.payment().timeoutMs()).isGreaterThan(0);
        assertThat(config.payment().maxRetry()).isBetween(0, 5);
    }

    @Test
    void securityConfigShouldBeValid() {
        assertThat(config.security().jwtSecret()).isNotBlank();
        assertThat(config.security().jwtExpirationSeconds())
            .isGreaterThanOrEqualTo(300);
    }
}

Özet

  • Konfigürasyon hiyerarşisi: Command line > env variable > application-{profile}.yml > application.yml — üstteki alttakini ezer

  • @ConfigurationProperties > @Value: Type-safe, validasyon destekli, IDE-friendly, refactoring-uyumlu

  • Secret management: Hardcoded ❌ → Environment variable ✅ → Vault/Secret Store ✅✅

  • Profile sistemi: Her ortam (dev/staging/prod) için ayrı konfigürasyon dosyası kullanın

  • Fail-fast: @Validated ile yanlış konfigürasyonda uygulama başlamasın — runtime sürprizi olmasın

  • Metadata: spring-boot-configuration-processor ile IDE auto-complete ve dokümantasyon