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
Environment variable (recommended for production)
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 config —
if (profile == "prod") { ... }in your code means you’re not using profiles correctly. Use@Profilebeans or configuration properties. - Forgetting to set the profile — the app runs with no profile active, using only
application.ymldefaults. 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.ymldefeats the purpose. It should come from the environment.