Spring Profiles

About

Spring Profiles provide a mechanism to separate environment-specific bean definitions and configuration logic, so our application can adapt automatically to different runtime contexts (e.g., development, testing, staging, production).

Instead of manually commenting/uncommenting configuration code for each environment, we can annotate beans or configuration classes with @Profile so they are only loaded if the matching profile is active.

Purpose

Spring Profiles exist to isolate and control bean registration based on runtime context, enabling one application codebase to serve multiple operational needs without invasive code changes.

In modern applications, environments differ in infrastructure, performance requirements, security policies, and integration endpoints. If we tried to support all environments with a single unfiltered configuration, we would face:

  • Complex conditional logic scattered across the codebase

  • High risk of misconfiguration, especially when moving from lower to higher environments

  • Hard-to-reproduce bugs caused by unintended bean inclusion

Spring Profiles solve this by giving the container selective visibility into which beans should be considered at startup. The decision is made before bean instantiation, so irrelevant beans are never even created.

Key reasons profiles exist:

  1. Environment Specialization

    • Dev might use an in-memory H2 database; prod might use a cloud-hosted Postgres.

    • Allows infrastructure changes without touching core logic.

  2. Behavior Swapping

    • Swap a real payment gateway with a mock gateway in tests.

    • Replace complex external integrations with local stubs for faster local development.

  3. Deployment Flexibility

    • The same artifact (JAR/WAR) can be deployed to multiple environments by simply changing an activation flag - no rebuild required.

  4. Risk Reduction

    • Prevents accidental use of production-only beans in development/testing contexts.

    • Eliminates “wrong environment” incidents by physically excluding beans.

  5. Clean Separation of Concerns

    • Configuration is moved out of business logic into declarative annotations or config files.

    • Developers no longer need to read conditional code to understand environment rules.

  6. Scalability in Large Teams

    • Teams working on different modules can define profile-specific beans without interfering with each other’s configurations.

    • Encourages modularity in infrastructure definitions.

  7. Runtime Adaptability

    • Supports complex setups like multi-cloud deployments, hybrid on-prem/cloud, or feature-flag-driven releases, where bean composition changes based on active profile combinations.

In short, the purpose of Spring Profiles is not just “different config for different environments”, but to build flexible, safe, and maintainable application configurations that scale across multiple deployment contexts - all while keeping the main codebase clean and environment-agnostic.

How It Works ?

Spring Profiles operate at the bean definition registration stage - before any beans are instantiated, but after all configuration sources have been loaded into the Environment.

Here’s the step-by-step breakdown:

1. Profile Resolution Phase

When the Spring application starts, the container determines which profiles are active and which are default:

  1. Sources for profile activation (checked in priority order):

    • Command-line arguments (--spring.profiles.active=...)

    • JVM system properties (-Dspring.profiles.active=...)

    • Environment variables (SPRING_PROFILES_ACTIVE)

    • Application properties/YAML files

    • Programmatic calls (ConfigurableEnvironment#setActiveProfiles)

  2. If no active profiles are explicitly set, the container:

    • Falls back to spring.profiles.default

    • If spring.profiles.default is also not set, uses "default" as the implicit profile.

2. Bean Definition Phase

Before bean instantiation:

  • Spring parses all @Configuration classes, @Component classes, and imported configurations.

  • Each bean definition is checked for profile metadata:

    • Beans with no @Profile → eligible for all profiles.

    • Beans with @Profile → only eligible if at least one profile matches the currently active set.

3. Matching Logic

Spring uses set intersection logic:

  • Let A = Set of active profiles (from Environment)

  • Let B = Set of profiles declared on the bean via @Profile

  • If A ∩ B is non-empty → Bean is registered.

  • If empty → Bean definition is discarded.

4. Profile Expressions

Spring 4+ allows logical operators in @Profile:

  • @Profile({"dev", "test"}) → OR condition

  • @Profile("dev & mysql") → AND condition

  • @Profile("!prod") → NOT condition

Expression parsing is handled by ProfilesParser, which creates a predicate evaluated against the active profile set.

5. Bean Registration Outcome

After profile filtering:

  • Only matching beans remain in the BeanDefinitionRegistry.

  • Dependency injection operates only on the filtered set.

  • Beans excluded due to profile mismatch:

    • Are never instantiated

    • Do not participate in dependency resolution

    • Do not trigger @PostConstruct or InitializingBean hooks

6. Special Cases

  • Overlapping profiles: Multiple beans for the same type may be active if profiles overlap. Use @Primary or @Qualifier to resolve ambiguity.

  • Profile-specific configuration classes: If a @Configuration class is annotated with @Profile, all beans inside are subject to the same profile restriction.

  • Default profile override: If spring.profiles.active is set, the default profile is ignored.

Naming Convention

The file naming must follow:

application-<profile>.yaml
application-<profile>.properties

Where <profile> exactly matches the profile name declared in:

  • spring.profiles.active

  • SPRING_PROFILES_ACTIVE environment variable

  • CLI argument (--spring.profiles.active=...)

  • Programmatic activation.

Declaring Profiles

In Spring, declaring a profile means associating a bean definition (or configuration class) with one or more environment identifiers so that the bean is only loaded when those profiles are active. This declaration is a filtering mechanism during the bean registration phase, not during bean execution.

When the container is starting up, it evaluates all bean definitions against the currently active profile set before even instantiating them. If the bean’s @Profile condition is not satisfied, the bean is never registered in the ApplicationContext. This is important because:

  • The bean's lifecycle methods (@PostConstruct, @PreDestroy) are never called.

  • Other beans cannot inject it because it doesn’t exist in the context.

  • This is more efficient than loading all beans and disabling them at runtime.

1. Applying @Profile on Bean Classes

When placed on a class that is a Spring bean (@Component, @Service, @Repository, @Controller), @Profile tells Spring to register that bean only when the matching profile is active.

@Service
@Profile("dev")
public class DevPaymentService implements PaymentService {
    public void pay() { System.out.println("Processing payment in DEV mode"); }
}
  • Effect: Only available when spring.profiles.active=dev (or when dev is part of a multi-profile setup).

  • If dev is not active, the container will not even know this bean exists.

2. Applying @Profile on @Bean Methods

Profiles can also be declared at the method level inside a @Configuration class. This allows us to conditionally define beans without splitting the configuration into multiple classes.

@Configuration
public class DataSourceConfig {

    @Bean
    @Profile("prod")
    public DataSource prodDataSource() {
        return new HikariDataSource(); // connects to production DB
    }

    @Bean
    @Profile("test")
    public DataSource testDataSource() {
        return new EmbeddedDatabaseBuilder().setType(H2).build(); // in-memory DB
    }
}
  • Here, only one DataSource bean is created depending on the active profile.

  • This prevents duplicate beans and ambiguity errors.

3. Applying @Profile on Configuration Classes

When applied at the class level to a @Configuration class, all bean methods inside inherit that profile condition.

@Configuration
@Profile("staging")
public class StagingConfiguration {

    @Bean
    public EmailService stagingEmailService() {
        return new SmtpEmailService("staging-mail.company.com");
    }

    @Bean
    public PaymentService stagingPaymentService() {
        return new PaymentGateway("https://staging-payments.company.com");
    }
}
  • This approach keeps environment-specific configurations grouped together.

  • Useful when multiple beans change together between environments.

4. Multi-Profile Declarations

We can assign multiple profiles to the same bean or configuration class:

@Component
@Profile({"qa", "staging"})
public class QAAndStagingLogger implements Logger { ... }
  • The bean will be active if any listed profile matches.

  • This is an OR condition by default.

5. Profile Expressions for Complex Conditions

Spring 4+ supports logical profile expressions:

  • AND: "dev & mysql" → both must be active.

  • OR: "dev | qa" → either one is active.

  • NOT: "!prod" → active in all profiles except prod.

Example:

@Component
@Profile("dev & !cloud")
public class LocalDevFileStorage implements FileStorage { ... }
  • Bean loads only in local dev environments, not in cloud deployments.

6. Default Profile Binding

If we don’t explicitly set a profile, beans without a @Profile annotation are considered profile-neutral - they load in all environments. But we can explicitly bind beans to the default profile:

@Component
@Profile("default")
public class DefaultMetricsService implements MetricsService { ... }
  • This is useful for having a fallback configuration when no profiles are active.

Default Profile

In Spring, the default profile is a fallback profile that is applied when no explicit profile is active.

  • It is internally represented as "default".

  • Beans annotated with @Profile("default") are only loaded if:

    • No profile is set via spring.profiles.active (properties, YAML, env vars, CLI, etc.).

    • No profile is activated programmatically.

  • Beans without any @Profile annotation are not tied to any profile and are always loaded regardless of active profile(s).

No @Profile → bean is always included.

@Profile("default") → bean is included only if nothing else is active.

Why Have a Default Profile ?

  • Local development fallback: When running the app without specifying a profile, it still starts with sensible defaults.

  • Baseline configuration: Shared settings that don’t belong to any specific environment but are needed for the app to run.

  • Avoid startup failures: Ensures the application has at least one set of beans to load.

How It Works Internally ?

  • At startup, Spring checks for active profiles:

    • If none are set → "default" profile is automatically active.

    • If one or more profiles are active → "default" profile is not added unless explicitly included in spring.profiles.active.

  • The "default" profile is just a label like any other; it’s not special beyond the auto-activation rule.

application.yaml vs application-<profile>.yaml

Spring Boot automatically loads configuration files based on active profiles:

Loading Order:

  1. application.yaml (or .properties) → Always loaded first.

  2. application-<profile>.yaml → Loaded only if <profile> is active.

  3. Profile-specific files override values from the base file.

A. Example – No Active Profile

Files:

# application.yaml
spring:
  datasource:
    url: jdbc:h2:mem:default
# application-dev.yaml
spring:
  datasource:
    url: jdbc:mysql://localhost/devdb

Run without spring.profiles.active:

  • Active profile → "default"

  • Loaded configs:

    • application.yaml (H2 in-memory DB)

    • application-dev.yaml is ignored.

B. Example – Active Profile is dev

Run with:

java -jar app.jar --spring.profiles.active=dev
  • Active profile → "dev"

  • Loaded configs:

    • application.yaml (base config)

    • application-dev.yaml (overrides datasource URL to MySQL)

  • "default" profile beans are ignored.

C. Example – Multiple Profiles

java -jar app.jar --spring.profiles.active=dev,aws
  • application.yaml (base config)

  • application-dev.yaml (dev-specific overrides)

  • application-aws.yaml (AWS-specific overrides)

  • All beans with @Profile("dev") or @Profile("aws") are eligible.

Activating Profiles

A. Properties/YAML

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

B. Command-Line

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

C. JVM System Property

java -Dspring.profiles.active=staging -jar app.jar

D. Environment Variable

export SPRING_PROFILES_ACTIVE=test

E. Programmatically

SpringApplication app = new SpringApplication(MyApp.class);
app.setAdditionalProfiles("dev");
app.run(args);

F. Multi-Profile Beans

A single bean can match multiple profiles:

@Profile({"qa", "staging"})
@Component
public class StagingPaymentService implements PaymentService { ... }

If either profile is active, the bean will be loaded.

G. Testing with Profiles

In tests, we can activate profiles with:

@SpringBootTest
@ActiveProfiles("test")
public class PaymentServiceTest { ... }

This ensures test beans replace production beans automatically.

H. Combining Profiles with Conditional Beans

@Profile can be combined with:

  • @ConditionalOnProperty

  • @ConditionalOnClass

  • @ConditionalOnMissingBean …for even more precise bean activation control.

Example:

@Bean
@Profile("cloud")
@ConditionalOnProperty(name = "storage.type", havingValue = "s3")
public FileStorage s3FileStorage() { ... }

Last updated