Spring Boot Profiles & Environments: @Profile, Environment-Specific Config

Manage environment-specific configuration in Spring Boot using profiles, @Profile annotation, and property file naming conventions.

published: reading time: 13 min read author: GeekWorkBench
Quick Summary

Manage environment-specific configuration in Spring Boot using profiles, @Profile annotation, and property file naming conventions. The guide uses practical examples to explain spring boot profiles & environments, introduction to spring profiles and shows how to apply the ideas in a Spring Boot project. It closes with common pitfalls and production checks so you can apply the pattern with fewer surprises.

Spring Boot Profiles & Environments

Introduction to Spring Profiles

Spring Profiles let you split your application configuration by environment. Instead of juggling multiple configuration files or sprinkling if (environment.equals(“prod”)) throughout your code, you activate a profile and Spring loads the right beans and properties automatically.

Your app runs in different contexts: dev, test, staging, production. Each context needs different configuration. Dev probably wants a local database and verbose logging. Production needs a managed database and minimal output. Profiles handle this cleanly—you ship one artifact and control behavior through external configuration.

Spring checks which profile is active and loads only the configuration that belongs to it. Same deployable artifact, different behavior per environment.

Activating Profiles

Using spring.profiles.active

The most common way to flip profiles is through the spring.profiles.active property. Several ways to set it:

In application.properties or application.yml:

spring.profiles.active=dev
spring:
  profiles:
    active: dev

Environment variable:

export SPRING_PROFILES_ACTIVE=dev,metrics

Command-line:

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

JNDI (if you happen to be in that world):

<Environment name="spring.profiles.active" value="prod" type="java.lang.String"/>

You can activate multiple profiles at once. When profiles conflict, later ones win.

Using @Profile Annotation

The @Profile annotation marks a bean or configuration class as profile-specific. The bean only gets created when that profile is active.

import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component;

@Component
@Profile("dev")
public class DevDataSource implements DataSource {
    // Development database configuration
}

You can also negate and combine profiles:

@Profile("!prod")              // Activates when NOT prod
@Profile("dev & !test")        // dev AND NOT test
@Profile({"dev", "local"})     // dev OR local

Profile-Specific Configuration Files

Spring Boot automatically picks up files named application-{profile}.yml or application-{profile}.properties.

File Hierarchy

src/
├── main/
│   ├── resources/
│   │   ├── application.yml           # Shared defaults
│   │   ├── application-dev.yml       # Dev overrides
│   │   ├── application-test.yml     # Test overrides
│   │   ├── application-staging.yml  # Staging overrides
│   │   └── application-prod.yml     # Production overrides

Spring merges the base file with the profile-specific file. Profile-specific values override base values.

Example Configuration Files

application.yml (base):

spring:
  application:
    name: myapp
  datasource:
    url: jdbc:postgresql://localhost:5432/mydb
    driver-class-name: org.postgresql.Driver

logging:
  level:
    root: INFO

application-dev.yml:

spring:
  datasource:
    url: jdbc:postgresql://localhost:5432/mydb_dev
    username: devuser
    password: devpass

logging:
  level:
    root: DEBUG
    com.myapp: TRACE

application-prod.yml:

spring:
  datasource:
    url: jdbc:postgresql://prod-db.internal:5432/mydb
    username: ${DB_USERNAME}
    password: ${DB_PASSWORD}
    hikari:
      maximum-pool-size: 20
      minimum-idle: 5

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

@Profile for Conditional Bean Registration

Beyond properties, @Profile controls which beans Spring creates. This is where things get interesting.

Real-World Example

public interface CacheManager {
    void put(String key, Object value);
    Object get(String key);
}

@Component
@Profile("dev")
public class InMemoryCacheManager implements CacheManager {
    private final Map<String, Object> cache = new ConcurrentHashMap<>();

    @Override
    public void put(String key, Object value) {
        cache.put(key, value);
    }

    @Override
    public Object get(String key) {
        return cache.get(key);
    }
}

@Component
@Profile("prod")
public class RedisCacheManager implements CacheManager {
    private final RedisTemplate<String, Object> redisTemplate;

    @Override
    public void put(String key, Object value) {
        redisTemplate.opsForValue().set(key, value, Duration.ofHours(24));
    }

    @Override
    public Object get(String key) {
        return redisTemplate.opsForValue().get(key);
    }
}

Depending on the active profile, your application gets either an in-memory cache or a Redis-backed one. No interface changes, no code changes. Just flip the profile.

Configuration Classes with @Profile

You can annotate entire @Configuration classes too:

@Configuration
@Profile("prod")
public class ProductionSecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain() {
        return SecurityFilterChainBuilder
            .create()
            .withOAuth2()
            .withRateLimiting()
            .build();
    }
}

The entire security configuration only loads in production. Dev never accidentally picks up production security rules.

Programmatic Profile Activation

Sometimes you need to set profiles in code rather than through configuration files.

Setting Active Profiles at Startup

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

public class MyApplication {
    public static void main(String[] args) {
        SpringApplication app = new SpringApplication(MyApplication.class);
        app.setAdditionalProfiles("dev", "local");
        app.run(args);
    }
}

Dynamically Activating Profiles

If you need custom logic, listen for the ApplicationEnvironmentPreparedEvent:

import org.springframework.context.annotation.Configuration;
import org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent;
import org.springframework.context.ApplicationListener;

@Configuration
public class ProfileActivationListener implements ApplicationListener<ApplicationEnvironmentPreparedEvent> {

    @Override
    public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) {
        Environment env = event.getEnvironment();

        String hostname = env.getProperty("HOSTNAME", "unknown");
        if (hostname.startsWith("prod-")) {
            env.addActiveProfile("prod");
        } else if (hostname.startsWith("staging-")) {
            env.addActiveProfile("staging");
        }
    }
}

This lets you auto-detect the environment based on hostname. Useful when you do not want to pass profile flags manually.

Profile Activation Order

Spring applies profiles in a specific order. Later profiles override earlier ones:

  1. Default profile (default)
  2. spring.profiles.default property
  3. spring.profiles.active property
  4. Command-line arguments
  5. Programmatic setAdditionalProfiles() calls

Profile-Specific Property Sources

Profiles work with Spring’s property source system beyond just YAML files.

Environment Variables

Spring maps environment variables to properties using a predictable naming convention:

# Sets datasource.url for the dev profile
SPRING_DATASOURCE_URL=jdbc:postgresql://localhost:5432/devdb

# Sets datasource.url for the prod profile
SPRING_PROD_DATASOURCE_URL=jdbc:postgresql://prod:5432/proddb

System Properties

java -Dspring.profiles.active=prod -Dserver.port=8443 -jar app.jar

Custom Property Sources

You can register property sources tied to a profile:

@Configuration
@Profile("cloud")
public class CloudConfigPropertiesConfig {

    @Bean
    public PropertySource<?> cloudPropertySource() {
        return new MapPropertySource("cloud", Map.of(
            "cloud.provider", "aws",
            "cloud.region", "us-east-1"
        ));
    }
}

When to Use / When NOT to Use

When to Use Profiles

  • Environment-specific database connections (dev vs prod)
  • Different bean implementations per environment
  • Logging levels that vary by deployment
  • External service endpoints that change across environments
  • Security configurations that differ by context
  • Feature flags tied to deployment context

When NOT to Use Profiles

  • Business logic variation across tenants: Profiles are not a multi-tenancy solution. Use proper tenancy architecture.
  • A/B testing feature toggles: Use a dedicated feature toggle system.
  • Runtime configuration changes: Profiles are fixed at startup. For dynamic config, look at Spring Cloud Config or Consul.
  • User-specific variation: Profiles are deployment-level, not user-level.
  • Constants that never change: If the value is the same everywhere, just hardcode it.

Common Pitfalls

1. Property Override Behavior

Spring merges properties from multiple sources in a set order. If a property is not overriding as expected, the property source ordering is probably the culprit. Debug it:

java -jar app.jar --debug | grep "PropertySource"

2. Missing Default Profile

No profile specified means Spring activates the default profile. If your base application.yml does not have the values you expect, things will silently fail or use wrong defaults.

3. Multiple Profiles with Conflicting Properties

spring.profiles.active=dev,aws  # aws overrides dev

Know your precedence when stacking profiles.

4. @Profile on Interface Implementation

If an interface itself has profile restrictions and no implementation matches the active profile, dependency injection will fail at startup. The interface should be unrestricted, with @Profile only on the implementations.

5. Test Profiles

Tests that do not specify a profile may pick up production configuration accidentally. Always be explicit:

@SpringBootTest
@ActiveProfiles("test")
class MyServiceTest {
    // ...
}

Security Notes

Sensitive Properties

Never commit secrets to profile-specific files. Use environment variable placeholders:

# application-prod.yml
spring:
  datasource:
    password: ${DB_PASSWORD}

Spring resolves ${DB_PASSWORD} from the environment at runtime. The actual password never touches version control.

Secret Scanning

Run pre-commit hooks to catch secrets before they enter your repository:

git secrets --install
git secrets --scan

Production Profile Lockdown

You can enforce explicit profile activation in production to prevent accidental dev configuration:

@Configuration
@Profile("prod")
public class ProductionProfileLock {

    @PostConstruct
    public void verifyProductionSetup() {
        if (System.getProperty("spring.profiles.active") == null) {
            throw new IllegalStateException(
                "Production profile must be explicitly activated"
            );
        }
    }
}

This fails fast if someone deploys to production without the right profile flag.

Implementation Snippets

Multi-Profile Activation with Maven

<profiles>
    <profile>
        <id>dev</id>
        <activation>
            <property>
                <name>env</name>
                <value>dev</value>
            </property>
        </activation>
        <properties>
            <spring.profiles.active>dev</spring.profiles.active>
        </properties>
    </profile>
    <profile>
        <id>prod</id>
        <activation>
            <property>
                <name>env</name>
                <value>prod</value>
            </property>
        </activation>
        <properties>
            <spring.profiles.active>prod</spring.profiles.active>
        </properties>
    </profile>
</profiles>

Profile-Specific Bean Conditions

@Configuration
public class DataSourceConfig {

    @Bean
    @Profile("dev")
    public DataSource devDataSource() {
        return DataSourceBuilder.create()
            .url("jdbc:h2:mem:devdb")
            .driverClassName("org.h2.Driver")
            .build();
    }

    @Bean
    @Profile("prod")
    public DataSource prodDataSource() {
        return DataSourceBuilder.create()
            .url(System.getenv("DB_URL"))
            .driverClassName("org.postgresql.Driver")
            .username(System.getenv("DB_USER"))
            .password(System.getenv("DB_PASSWORD"))
            .build();
    }
}

Conditional Configuration with @ConditionalOnProperty

@Configuration
@Profile("aws")
@ConditionalOnProperty(name = "aws.s3.enabled", havingValue = "true", matchIfMissing = true)
public class S3Config {

    @Bean
    public AmazonS3 amazonS3(AWSProperties properties) {
        return AmazonS3ClientBuilder.standard()
            .withRegion(properties.getRegion())
            .build();
    }
}

Observability Checklist

When using profiles in production, make sure you can see what profile is running:

  • Profile Display: Log the active profile at startup
  • Metrics Tagged by Profile: Add profile as a metric dimension
  • Logging Profile Context: Include profile name in correlation IDs
  • Configuration Validation: Check required properties exist for each profile
  • Startup Warnings: Alert when an unexpected profile activates
  • Health Checks: Verify profile-specific dependencies are reachable

Startup Profile Logging

# application.yml
spring:
  main:
    show-banner: true

logging:
  pattern:
    console: "%d{yyyy-MM-dd HH:mm:ss} [%profile] %-5level %logger{36} - %msg%n"

Actuator Profile Endpoint

management:
  endpoints:
    web:
      exposure:
        include: health,info,env,beans
  endpoint:
    health:
      show-details: always

Hit /actuator/env to see every property source and its values for the active profile. Useful when debugging why a property has an unexpected value.

Trade-off Table

Aspect Using Profiles Alternative Approaches
Complexity Low to medium Feature toggles (high), multi-tenancy (medium)
Startup time Minimal impact Conditional beans add slight overhead
Maintenance Single codebase Multiple branches or deployments
Flexibility Fixed at startup Runtime config (Spring Cloud Config)
Testing Easy to mock Requires profile switching
Documentation Implicit behavior Explicit toggle docs needed
Secret Management External required Centralized secret service

Quick Recap Checklist

  • Understand how profiles segregate configuration
  • Know multiple ways to activate profiles
  • Use application-{profile}.yml for environment-specific properties
  • Apply @Profile for conditional bean registration
  • Understand property precedence across profiles
  • Activate multiple profiles when needed
  • Set up test profiles with @ActiveProfiles
  • Protect sensitive values with environment variables
  • Validate profile activation in production
  • Log active profile at startup for debugging
  • Document which profile each environment uses
  • Use feature flags for runtime toggles, not profiles

Failure Scenarios

Scenario 1: Profile Not Activated in Production

Problem: Application starts with the default profile instead of prod. Wrong database, exposed dev endpoints.

Fix: Fail fast at startup:

@Configuration
@Profile("prod")
public class ProductionProfileValidator {

    @Autowired
    private ConfigurableEnvironment env;

    @PostConstruct
    public void validate() {
        if (!env.getActiveProfiles().contains("prod")) {
            throw new IllegalStateException("Attempted to run in production without prod profile");
        }
    }
}

Scenario 2: Missing Profile-Specific Property

Problem: Code references a property that only exists in one profile. Null values or exceptions follow.

Fix: Always provide defaults in application.yml. Use @Value with fallback values:

@Value("${cache.ttl:3600}")
private int cacheTtl;

Scenario 3: Circular Profile Dependencies

Problem: A bean with @Profile(“redis”) depends on another bean that is also profile-gated and might not be active.

Fix: Create a no-op default implementation:

@Component
public class DefaultCacheManager implements CacheManager {
    // Does nothing or uses a simple in-memory fallback
}

@Component
@Profile("redis")
public class RedisCacheManager implements CacheManager {
    // Real Redis implementation
}

The default is always present. The Redis profile overrides it when active.

Interview Questions

1. How does Spring Boot resolve property values when multiple profiles are active?

Spring Boot evaluates property sources in a fixed order: command-line arguments first, then environment variables, then application properties from profile-specific files, then defaults. When you activate multiple profiles like spring.profiles.active=dev,prod, the prod profile's properties override dev's for any conflicting keys. The order you specify matters. You can see the exact resolution order by running with --debug and checking the PropertySource output, or by inspecting ConfigurableEnvironment programmatically in code.

2. Can you use @Profile annotation with logical operators, and how?

Yes. The annotation uses SpEL under the hood, so you get negation with !, like @Profile("!prod") to activate when prod is not active. For AND logic, use & as in @Profile("dev & !test"), which reads as "dev AND NOT test." For OR logic, use an array: @Profile({"dev", "local"}) activates when either profile is active. If your expression gets complex, remember that & needs escaping as & inside an annotation string.

3. What happens if no profile is explicitly activated?

Spring Boot automatically activates the default profile. That means it looks for application-default.yml or application-default.properties if they exist—though most projects do not bother creating them. Your base application.yml loads regardless. You can change the default profile by setting spring.profiles.default. In production, always explicitly activate a profile rather than relying on defaults. You want to know exactly which configuration is running.

4. How do you handle sensitive configuration values in profile-specific files?

Never hardcode secrets in files that go into version control. Use placeholders like ${DB_PASSWORD} and let Spring resolve them from environment variables at runtime. For enterprise deployments, integrate with HashiCorp Vault, AWS Secrets Manager, or Azure Key Vault—these can inject secrets as environment variables during deployment so secrets never appear in config files. Run secret scanning tools on pre-commit to catch accidental credential commits before they reach your repository.

5. Explain the difference between @Profile and @ConditionalOnProperty. When would you use each?

@Profile gates beans based on which deployment profile is active—useful for swapping entire implementations between dev and prod, like using an in-memory cache versus Redis. @ConditionalOnProperty is broader: it gates beans based on any property value, not just the active profile. You would use it for feature flags, optional integrations, or configuration-driven behavior that might change independently of the deployment environment. They can be combined: @Profile("aws") @ConditionalOnProperty(name = "s3.enabled") only activates S3 beans when you are on AWS and explicitly enable S3.

Further Reading

Conclusion

Spring Profiles provide a clean mechanism for environment-specific configuration without resorting to multiple artifacts or conditional logic scattered throughout your codebase. The key is using them thoughtfully: activate profiles through environment variables for deployment flexibility, use @Profile annotations for conditional bean registration, and always protect sensitive values with environment variable placeholders rather than hardcoding secrets.

Profile-based configuration works best when combined with external configuration management for secrets and centralized configuration servers for runtime changes. Use profiles as the coarse-grained mechanism for environment differences (dev vs prod), and feature flags for fine-grained runtime toggles that may change independently of the deployment environment.

Category

Related Posts

Spring Boot Build Tools: Maven & Gradle

Configure Maven and Gradle for Spring Boot projects—plugins, dependency management, packaging JARs and WARs, and build automation essentials.

#spring-boot #spring-boot-roadmap #learning-path

Embedded Web Servers in Spring Boot: Tomcat, Jetty, Undertow

Configure embedded servers in Spring Boot: compare Tomcat, Jetty, and Undertow, tune thread pools, enable access logs, and switch implementations.

#spring-boot #spring-boot-roadmap #learning-path

JUnit 5 & Jupiter: Lifecycle, Nested & Parameterized Tests

Explore JUnit 5 Jupiter features: master test lifecycle annotations, organize tests with @Nested, and parameterize tests with @CsvSource and @MethodSource.

#spring-boot #spring-boot-roadmap #learning-path