Spring Basics 3 min read

application.properties vs application.yml in Spring Boot: Making the Right Choice

Explore the differences between application.properties and application.yml in Spring Boot. Learn when to use each format, how to implement type-safe configuration with Kotlin, and follow best practices for organizing your application settings.

Spring Boot offers two popular ways to configure your application: properties files and YAML files. In this guide, we'll explore both approaches, their advantages, and help you choose the right format for your project.

Key Takeaways

  • Understanding the differences between .properties and .yml formats
  • When to choose each format
  • How to use configuration properties with Kotlin data classes
  • Best practices for organizing your configuration

Basic Comparison

Let's start with a simple comparison using the same configuration in both formats:

# application.properties
server.port=8080
spring.datasource.url=jdbc:postgresql://localhost:5432/mydb
spring.datasource.username=user
spring.datasource.password=secret
spring.jpa.hibernate.ddl-auto=update
app.api.version=v1
app.api.security.enabled=true
app.api.security.token-expiration=3600
# application.yml
server:
  port: 8080
spring:
  datasource:
    url: jdbc:postgresql://localhost:5432/mydb
    username: user
    password: secret
  jpa:
    hibernate:
      ddl-auto: update
app:
  api:
    version: v1
    security:
      enabled: true
      token-expiration: 3600

Type-Safe Configuration with Kotlin

Spring Boot works beautifully with Kotlin data classes for type-safe configuration:

@ConfigurationProperties(prefix = "app.api")
@ConstructorBinding
data class ApiConfig(
    val version: String,
    val security: SecurityConfig
) {
    data class SecurityConfig(
        val enabled: Boolean,
        val tokenExpiration: Long
    )
}

// Enable configuration properties scanning
@SpringBootApplication
@ConfigurationPropertiesScan
class MyApplication

fun main(args: Array<String>) {
    runApplication<MyApplication>(*args)
}

// Usage in a service
@Service
class ApiService(private val apiConfig: ApiConfig) {
    fun getApiVersion() = apiConfig.version
    fun isSecurityEnabled() = apiConfig.security.enabled
}

Environment-Specific Configuration

Both formats support profile-specific configuration:

# application-dev.properties
app.api.security.enabled=false
logging.level.root=DEBUG

# application-prod.properties
app.api.security.enabled=true
logging.level.root=INFO
# application.yml
spring:
  profiles:
    active: dev
---
spring:
  config:
    activate:
      on-profile: dev
app:
  api:
    security:
      enabled: false
logging:
  level:
    root: DEBUG
---
spring:
  config:
    activate:
      on-profile: prod
app:
  api:
    security:
      enabled: true
logging:
  level:
    root: INFO

Lists and Arrays

YAML shines when dealing with lists and arrays:

# application.properties
app.cors.allowed-origins[0]=https://example.com
app.cors.allowed-origins[1]=https://dev.example.com
app.cors.allowed-methods=GET,POST,PUT,DELETE
# application.yml
app:
  cors:
    allowed-origins:
      - https://example.com
      - https://dev.example.com
    allowed-methods:
      - GET
      - POST
      - PUT
      - DELETE

Here's how to handle this in Kotlin:

@ConfigurationProperties(prefix = "app.cors")
data class CorsConfig(
    val allowedOrigins: List<String>,
    val allowedMethods: List<String>
)

@Configuration
class WebConfig(private val corsConfig: CorsConfig) : WebMvcConfigurer {
    override fun addCorsMappings(registry: CorsRegistry) {
        registry.addMapping("/api/**")
            .allowedOrigins(*corsConfig.allowedOrigins.toTypedArray())
            .allowedMethods(*corsConfig.allowedMethods.toTypedArray())
    }
}

Complex Objects

YAML's hierarchical structure makes it easier to read complex configurations:

# application.yml
app:
  cache:
    providers:
      redis:
        host: localhost
        port: 6379
        database: 0
      memory:
        maximum-size: 1000
        eviction-policy: LRU
  scheduling:
    tasks:
      cleanup:
        cron: "0 0 * * * *"
        enabled: true
      report-generation:
        cron: "0 0 0 * * *"
        enabled: false

The corresponding Kotlin classes:

@ConfigurationProperties(prefix = "app")
data class AppConfig(
    val cache: CacheConfig,
    val scheduling: SchedulingConfig
) {
    data class CacheConfig(
        val providers: Providers
    ) {
        data class Providers(
            val redis: RedisConfig,
            val memory: MemoryConfig
        )
        
        data class RedisConfig(
            val host: String,
            val port: Int,
            val database: Int
        )
        
        data class MemoryConfig(
            val maximumSize: Int,
            val evictionPolicy: String
        )
    }
    
    data class SchedulingConfig(
        val tasks: Map<String, TaskConfig>
    ) {
        data class TaskConfig(
            val cron: String,
            val enabled: Boolean
        )
    }
}

When to Choose Each Format

Choose Properties (.properties) when:

  1. You need maximum compatibility
  2. Your configurations are simple and flat
  3. You're working with legacy systems
  4. You want to use system property overrides easily

Choose YAML (.yml) when:

  1. You have hierarchical configuration data
  2. You need to maintain multiple profiles in one file
  3. You work with lists and arrays frequently
  4. You want better readability for complex configurations

Best Practices

  1. Be Consistent
    • Choose one format and stick to it
    • Don't mix .properties and .yml files unless necessary
  2. Use Profile-Specific Files
    • Keep base configuration in application.yml
    • Override in application-{profile}.yml

Document Your Configuration

@ConfigurationProperties(prefix = "app")
data class AppConfig(
    /** Application name used in logs and metrics */
    val name: String,
    /** Current API version number */
    val version: Int
)

Use Type-Safe Configuration

@ConfigurationProperties(prefix = "app")
@Validated
data class AppConfig(
    @field:NotBlank
    val name: String,
    @field:Min(1)
    val version: Int,
    val features: Features
) {
    data class Features(
        val enabled: Boolean = false,
        val maxItems: Int = 10
    )
}

Common Pitfalls

  1. YAML Indentation
    • YAML is sensitive to indentation
    • Use spaces, not tabs
    • Maintain consistent indentation
  2. Property Name Case
    • Use kebab-case in files: token-expiration
    • Use camelCase in Kotlin: tokenExpiration
    • Don't forget to activate profiles:

Profile Activation

@Profile("dev")
@Configuration
class DevConfig

Next Steps

  • Learn about externalized configuration
  • Explore Spring Cloud Config for distributed systems
  • Study Spring Boot's relaxed binding rules
  • Implement configuration validation

Remember: Both formats are equally powerful, and Spring Boot handles them seamlessly. Choose the one that best fits your team's needs and coding style.