What Are Profiles, and Why Do They Exist?
A Spring profile is a named configuration scope. When a profile is active, Spring loads its profile-specific property files and registers only the beans annotated for that profile โ everything else stays exactly the same. The result is one application binary that behaves differently in development, CI, and production without any code changes between environments.
This fits directly into the property source priority system covered in
Application Properties: profile-specific
files (application-prod.properties) sit higher in the priority
order than the default application.properties, so they
selectively override only the keys that genuinely differ per environment
while the shared base file covers the rest.
If spring.profiles.active is not set anywhere, Spring Boot
loads only application.properties / application.yml
โ no profile-specific file is touched. The implicit fallback is
default: a file named
application-default.properties is loaded when no
other profile is active, which is useful for local dev defaults you
don't want to commit as the universal base config.
Profile-Specific Property Files
src/main/resources/
โโโ application.properties # Shared defaults โ loaded always
โโโ application-dev.properties # Loaded only when "dev" profile is active
โโโ application-test.properties # Loaded only when "test" profile is active
โโโ application-prod.properties # Loaded only when "prod" profile is active
# application.properties โ keys shared by ALL environments
app.name=My Application
spring.jpa.open-in-view=false
# application-dev.properties โ dev overrides only
spring.datasource.url=jdbc:h2:mem:devdb
spring.jpa.hibernate.ddl-auto=create-drop # destroys schema on shutdown โ fine for dev, never for prod
spring.jpa.show-sql=true
logging.level.com.example=DEBUG
# application-prod.properties โ prod overrides only
spring.datasource.url=${DB_URL} # resolved from env var โ never hardcoded here
spring.jpa.hibernate.ddl-auto=validate
logging.level.root=WARN
ddl-auto=create-drop drops and recreates the entire schema
every time the application starts and every time it stops. Any
data you inserted during a dev session is gone the moment you stop the
app. This is intentional for a truly disposable dev database, but
developers who expect data to persist between restarts hit this hard the
first time. The alternative for dev is update (schema
evolves, data survives restarts) โ still never acceptable in production,
but less destructive for iterative dev work.
Activating Profiles
# In application.properties โ useful for local dev, never commit "prod" here
spring.profiles.active=dev
# Command-line argument โ overrides the file, useful for one-off runs
java -jar app.jar --spring.profiles.active=prod
# Environment variable โ the correct approach in containers and CI/CD
export SPRING_PROFILES_ACTIVE=prod
java -jar app.jar
# Multiple profiles โ all their property files are loaded, all their @Profile beans register
java -jar app.jar --spring.profiles.active=prod,metrics,featureX
Setting spring.profiles.active=prod inside
application.properties and committing it means every
developer who clones the repo runs production config locally โ wrong
database, wrong log level, wrong everything. The convention is:
application.properties sets active=dev (or
nothing), and production activation is handled exclusively by the
deployment environment via SPRING_PROFILES_ACTIVE.
Profile-Conditional Beans
// Two implementations of the same interface โ only one registers per profile
@Service
@Profile("dev")
public class MockEmailService implements EmailService {
@Override
public void send(String to, String body) {
log.info("[DEV] Fake email to {}: {}", to, body); // no real email sent
}
}
@Service
@Profile("prod")
public class SmtpEmailService implements EmailService {
@Override
public void send(String to, String body) { /* real SMTP */ }
}
@Configuration
public class DataSourceConfig {
// Registers in any profile except prod โ dev, test, staging, local, etc.
@Bean
@Profile("!prod")
public DataSource embeddedDataSource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.H2)
.build();
}
// Prod DataSource reads its config from @ConfigurationProperties โ no hardcoded URLs here
@Bean
@Profile("prod")
public DataSource prodDataSource(DataSourceProperties props) {
return props.initializeDataSourceBuilder().build();
// spring.datasource.url/username/password are resolved from env vars in prod
}
// Seed data loader โ only outside prod, AND only when the "seed" feature profile is active
@Bean
@Profile({"dev", "test"})
public DataInitializer dataInitializer() {
return new DataInitializer();
}
}
@Profile("!prod") means "active if prod is NOT active."
@Profile({"dev", "test"}) means "active if dev OR test is
active." For AND logic โ "active only if BOTH staging AND metrics are
active" โ use the expression syntax:
@Profile("staging & metrics"). This keeps the condition
on the bean itself rather than duplicating it across multiple callers.
YAML Multi-Document โ All Profiles in One File
# application.yml โ shared defaults (no profile block)
app:
name: My Application
spring:
jpa:
open-in-view: false
---
# Document 2 โ active only when profile "dev" is active
spring:
config:
activate:
on-profile: dev
datasource:
url: jdbc:h2:mem:devdb
jpa:
hibernate:
ddl-auto: create-drop
show-sql: true
---
# Document 3 โ active only when profile "prod" is active
spring:
config:
activate:
on-profile: prod
datasource:
url: "${DB_URL}"
jpa:
hibernate:
ddl-auto: validate
logging:
level:
root: WARN
YAML does not allow two keys with the same name at the same level in the
same document. Writing spring: twice in one
----separated block โ once for
spring.config.activate and once for
spring.datasource โ means the second spring:
block silently overwrites the first. Everything under
spring.config.activate disappears, the profile activation
condition is lost, and the document loads for every profile. The fix,
as shown above, is a single spring: key per document,
with all spring-namespaced config nested beneath it.
Profiles in Tests
// @ActiveProfiles activates the named profile for the entire test class
@SpringBootTest
@ActiveProfiles("test")
class UserServiceIntegrationTest {
// Loads application.properties + application-test.properties
// Registers all @Profile("test") beans, none of the @Profile("prod") ones
}
// @DataJpaTest already activates an embedded database โ combine with a custom profile
// to load test-specific seed data or configuration without a full context
@DataJpaTest
@ActiveProfiles("test")
class UserRepositoryTest { ... }
# application-test.properties โ test-specific overrides
spring.datasource.url=jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1
spring.jpa.hibernate.ddl-auto=create-drop
logging.level.root=WARN # suppress noise during test runs
@DataJpaTest auto-configures an in-memory H2 database by
default, replacing whatever spring.datasource.* is
configured. An explicit application-test.properties with
H2 settings is harmless but redundant for repository slice tests. It
becomes genuinely necessary when using
@AutoConfigureTestDatabase(replace = Replace.NONE) with
Testcontainers against a real database โ see
Spring Data JPA โ Testing.
Interview Questions
Q: What happens if no profile is active?
Only application.properties / application.yml is
loaded. No profile-specific file is touched. If an
application-default.properties file exists, Spring Boot loads
it as the implicit fallback when no other profile is active.
Q: Can multiple profiles be active at the same time?
Yes โ --spring.profiles.active=prod,metrics activates both.
All matching property files are loaded and all matching
@Profile beans register. Properties from later-activated
profiles override earlier ones for the same key.
Q: Where should spring.profiles.active be set, and what should never set it?
Production activation belongs exclusively in the deployment environment โ
a container env var (SPRING_PROFILES_ACTIVE=prod), a CI/CD
pipeline variable, or a Kubernetes ConfigMap. The only legitimate use of
spring.profiles.active inside a committed
application.properties is setting dev as a
local convenience default, with the explicit expectation that any real
environment overrides it. Committing active=prod to a shared
repo is a configuration management failure, not a style preference.
Q: What's the YAML multi-document bug with duplicate top-level keys, and why is it silent?
YAML's spec disallows duplicate keys at the same level in the same
mapping โ but many parsers (including SnakeYAML, which Spring uses) treat
this as a "last key wins" situation rather than an error. Writing
spring: twice in one document means the first block is
silently discarded โ the profile activation condition in
spring.config.activate.on-profile disappears, and the
document is loaded unconditionally for all profiles. The fix is a single
spring: root key per document, with all spring-namespaced
properties nested under it.