Spring Configuration & Profiles

Environment Profiles (@Profile)

18 min Lesson 6 of 13

Environment Profiles (@Profile)

Every serious application runs in more than one environment: a developer's laptop, a shared QA server, a staging environment, and production. Each of those environments typically needs different infrastructure — an in-memory H2 database locally, a real PostgreSQL instance in staging, encrypted credentials in production. Duplicating configuration classes per environment or hiding every difference behind a forest of if statements is fragile and error-prone.

Spring's profile mechanism solves this cleanly. A profile is a named logical grouping. You annotate beans or entire configuration classes with @Profile, activate one or more profiles at startup, and Spring registers only the beans whose profiles match. Everything else is ignored — safely, at the container level, not with runtime conditionals scattered through your code.

Declaring a Profile-Specific Bean

The @Profile annotation accepts a single string or an array of strings. A bean annotated with @Profile("dev") is registered only when the dev profile is active.

import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Profile; import javax.sql.DataSource; import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType; @Configuration public class DataSourceConfig { @Bean @Profile("dev") public DataSource devDataSource() { // Fast in-memory database — no Docker, no VPN needed locally return new EmbeddedDatabaseBuilder() .setType(EmbeddedDatabaseType.H2) .addScript("classpath:schema.sql") .addScript("classpath:test-data.sql") .build(); } @Bean @Profile("prod") public DataSource prodDataSource() { // Real connection pool pointing at production Postgres var config = new com.zaxxer.hikari.HikariConfig(); config.setJdbcUrl(System.getenv("DB_URL")); config.setUsername(System.getenv("DB_USER")); config.setPassword(System.getenv("DB_PASS")); config.setMaximumPoolSize(20); return new com.zaxxer.hikari.HikariDataSource(config); } }

Spring evaluates @Profile at container startup. If no active profile matches, the bean is simply not registered. If your code injects DataSource without a qualifier and neither profile is active, you get a NoSuchBeanDefinitionException at startup — which is exactly the right failure mode: loud, early, before any request is served.

Annotating an Entire Configuration Class

When an entire class is only relevant to one environment, annotate the class itself rather than each individual @Bean method. This is cleaner and makes the intent obvious:

@Configuration @Profile("staging") public class StagingEmailConfig { @Bean public JavaMailSender mailSender() { // Points at a local MailHog/Mailpit instance — emails never leave the network SimpleMailMessage defaults = new SimpleMailMessage(); JavaMailSenderImpl sender = new JavaMailSenderImpl(); sender.setHost("localhost"); sender.setPort(1025); return sender; } @Bean public String supportEmail() { return "staging-support@internal.example.com"; } }
Profile on a class vs. on a method: When @Profile is on the class, every @Bean method inside inherits it. When it is on a single @Bean method, only that bean is profile-gated. Use class-level annotation when the whole configuration is environment-specific; use method-level when you are mixing shared and profile-specific beans in the same class.

Profile Expressions — Combining Profiles Logically

Since Spring 5.1, the string inside @Profile supports a simple expression language that lets you write conditions without multiple annotations:

  • @Profile("dev") — active when dev is active.
  • @Profile("!prod") — active when prod is not active (useful for safety guards).
  • @Profile("dev | staging") — active when either is active (OR logic).
  • @Profile("cloud & eu-west") — active only when both are active (AND logic).
  • @Profile("(dev | staging) & !legacy") — grouped logical expressions.
@Bean @Profile("!prod") public DataSource mockPaymentGateway() { // Always register a mock payment gateway unless we are in production. // This prevents accidental real charges in any non-prod environment, // even if someone forgets to activate the "dev" profile explicitly. return new MockPaymentDataSource(); }
Use negation guards for safety-critical beans. Annotating a stub or mock with @Profile("!prod") is more defensive than annotating the real implementation with @Profile("prod"). If someone runs the app without any profile the stub loads — which is far safer than accidentally loading a production integration.

Activating Profiles

Spring reads the active profiles from several sources, evaluated in priority order. The most common mechanisms are:

1. In application.properties / application.yml:

# application.properties spring.profiles.active=dev
# application.yml spring: profiles: active: staging

This is convenient for local defaults but should not be committed with prod — otherwise every developer who clones the repo accidentally points at production.

2. As a JVM system property (highest precedence from command line):

java -jar myapp.jar -Dspring.profiles.active=prod

3. As an OS environment variable (preferred for containers and CI/CD):

SPRING_PROFILES_ACTIVE=prod java -jar myapp.jar

Environment variables follow Spring Boot's relaxed-binding rules: dots become underscores, names are uppercased. This matches the conventions of Docker, Kubernetes, and most CI platforms, making it the idiomatic production activation mechanism.

4. Programmatically — useful in tests:

import org.springframework.test.context.ActiveProfiles; import org.springframework.boot.test.context.SpringBootTest; @SpringBootTest @ActiveProfiles("test") class PaymentServiceIntegrationTest { // Spring activates the "test" profile for this test class only }

Multiple profiles can be active simultaneously. Separate them with commas in the property or environment variable, or supply multiple values to @ActiveProfiles:

SPRING_PROFILES_ACTIVE=cloud,eu-west,feature-x

Profile-Specific Property Files

Spring Boot extends the profile mechanism to property files automatically. If the active profile is staging, Boot loads both application.properties and application-staging.properties (or the YAML equivalents). The profile-specific file's values override the base file's values.

# application.properties (shared defaults) server.port=8080 logging.level.root=INFO # application-dev.properties (developer overrides) server.port=8090 logging.level.com.example=DEBUG spring.datasource.url=jdbc:h2:mem:devdb # application-prod.properties (production values — do NOT commit secrets here) logging.level.root=WARN management.endpoints.web.exposure.include=health,info
Never commit production secrets to profile property files. application-prod.properties in version control is the right place for non-secret production settings (log levels, endpoint exposure), but database passwords and API keys must come from environment variables or a secrets manager — even in profile-specific files.

The Default Profile

Beans annotated with @Profile("default") are registered when no profile is active. This is a useful safety net: supply a sensible fallback (usually a local stub) so the application starts cleanly even without an explicit profile, rather than throwing a missing-bean error.

@Bean @Profile("default") public DataSource fallbackDataSource() { // Loaded when no profile is set — handy for IDE runs with no -D flags return new EmbeddedDatabaseBuilder() .setType(EmbeddedDatabaseType.H2) .build(); }

Summary

The @Profile annotation gives you a first-class, Spring-managed way to vary your bean graph per environment. Annotate beans or whole configuration classes with a profile name; use expression syntax (!, |, &) for richer conditions. Activate profiles via environment variables for containers, via application.properties for developer defaults, and via @ActiveProfiles in tests. Combine @Profile with profile-specific property files (application-{profile}.properties) to keep every environment's configuration collocated, readable, and free of runtime if checks.