Skip to main content
PCSalt logo
YouTube GitHub
Back to Spring Boot
Spring Boot · 2 min read

Spring Boot Profiles Done Right — Config, Activation & Secrets

Master Spring Boot profiles — profile-specific configuration files, conditional beans, type-safe @ConfigurationProperties, and secrets management across environments.


Every application runs in multiple environments — local dev, staging, production. Database URLs, feature flags, API keys, and logging levels differ between them. Spring Boot profiles let you manage these differences without if statements scattered through your code.

Profile-specific configuration

Spring Boot loads application.yml first, then overlays profile-specific files on top.

application.yml — shared defaults:

spring:
  application:
    name: order-service

server:
  port: 8080

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

application-dev.yml:

spring:
  data:
    mongodb:
      uri: mongodb://localhost:27017/orders-dev

logging:
  level:
    com.myapp: DEBUG
    org.springframework.data.mongodb: DEBUG

app:
  features:
    email-notifications: false

application-prod.yml:

spring:
  data:
    mongodb:
      uri: ${MONGODB_URI}

logging:
  level:
    com.myapp: INFO
    root: WARN

app:
  features:
    email-notifications: true

Dev gets a local MongoDB and verbose logging. Prod reads the connection string from an environment variable and keeps logs quiet.

Multi-document profiles in one file

For small projects, you can use YAML document separators instead of separate files:

spring:
  application:
    name: order-service
---
spring:
  config:
    activate:
      on-profile: dev
  data:
    mongodb:
      uri: mongodb://localhost:27017/orders-dev
---
spring:
  config:
    activate:
      on-profile: prod
  data:
    mongodb:
      uri: ${MONGODB_URI}

This works but gets unwieldy as config grows. Separate files are easier to maintain once you have more than a few overrides.

Activating profiles

export SPRING_PROFILES_ACTIVE=prod
java -jar order-service.jar

Command line

java -jar order-service.jar --spring.profiles.active=prod

Docker

ENV SPRING_PROFILES_ACTIVE=prod

Or in docker-compose.yml:

services:
  order-service:
    image: order-service:latest
    environment:
      - SPRING_PROFILES_ACTIVE=prod
      - MONGODB_URI=mongodb://mongo:27017/orders

Kubernetes

apiVersion: apps/v1
kind: Deployment
spec:
  template:
    spec:
      containers:
        - name: order-service
          env:
            - name: SPRING_PROFILES_ACTIVE
              value: prod
            - name: MONGODB_URI
              valueFrom:
                secretKeyRef:
                  name: order-service-secrets
                  key: mongodb-uri

Never hardcode spring.profiles.active in application.yml. It should always come from the deployment environment.

@Profile for conditional beans

Sometimes you need different implementations per environment — a real email sender in production, a logging stub in dev:

interface NotificationSender {
    fun send(to: String, message: String)
}

@Service
@Profile("prod")
class EmailNotificationSender(
    private val emailClient: EmailClient
) : NotificationSender {

    override fun send(to: String, message: String) {
        emailClient.send(to, "Notification", message)
    }
}

@Service
@Profile("dev")
class LoggingNotificationSender : NotificationSender {

    private val log = LoggerFactory.getLogger(javaClass)

    override fun send(to: String, message: String) {
        log.info("DEV notification to={}, message={}", to, message)
    }
}

Spring creates only the bean matching the active profile. Your service code injects NotificationSender without knowing which implementation it gets.

Profile negation

@Profile("!prod")
class MockPaymentGateway : PaymentGateway { ... }

This bean loads for every profile except prod.

Multiple profiles

@Profile("prod", "staging")
class RealPaymentGateway : PaymentGateway { ... }

@ConfigurationProperties — type-safe config

Instead of scattering @Value annotations everywhere, bind a whole config section to a Kotlin class:

import org.springframework.boot.context.properties.ConfigurationProperties
import jakarta.validation.constraints.Max
import jakarta.validation.constraints.Min

@ConfigurationProperties(prefix = "app.pagination")
data class PaginationProperties(
    @field:Min(1)
    @field:Max(100)
    val defaultSize: Int = 20,

    @field:Min(1)
    @field:Max(500)
    val maxSize: Int = 100
)

@ConfigurationProperties(prefix = "app.features")
data class FeatureProperties(
    val emailNotifications: Boolean = false
)

Enable scanning in your main class:

@SpringBootApplication
@ConfigurationPropertiesScan
class OrderServiceApplication

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

Inject the properties where needed:

@Service
class OrderService(
    private val pagination: PaginationProperties,
    private val features: FeatureProperties
) {

    fun getOrders(page: Int, size: Int): Page<Order> {
        val effectiveSize = size.coerceIn(1, pagination.maxSize)
        // ...
    }
}

You get compile-time type safety, IDE autocomplete, and validation at startup — if defaultSize is 0, the application won’t start.

Secrets management

Dev: defaults in config

# application-dev.yml
app:
  jwt:
    secret: dev-secret-not-for-production
  api:
    key: dev-api-key

Dev secrets are committed to the repo — they’re not real secrets, just local dev values.

Prod: environment variables

# application-prod.yml
app:
  jwt:
    secret: ${JWT_SECRET}
  api:
    key: ${API_KEY}

Production secrets come from environment variables, injected by your deployment platform (Kubernetes secrets, AWS Parameter Store, Vault).

Never commit production secrets to Git. Use ${PLACEHOLDER} syntax to fail fast if the variable isn’t set.

For more on secrets management patterns, see the Spring Boot Security post.

Common mistakes

  • Logic instead of configif (profile == "prod") { ... } in your code means you’re not using profiles correctly. Use @Profile beans or configuration properties.
  • Forgetting to set the profile — the app runs with no profile active, using only application.yml defaults. Fail fast by requiring critical properties that only exist in profile-specific files.
  • Secrets in Git — production passwords, API keys, and tokens should never be in version control. Use environment variables or a secrets manager.
  • Too many profiles — dev, staging, preprod, prod, local, test, integration… each one is maintenance. Keep it to dev + prod. If staging needs different config from prod, it’s usually just different environment variable values, not a different profile.
  • Hardcoding spring.profiles.active — setting the active profile in application.yml defeats the purpose. It should come from the environment.