Profiles & Environments

Environment-specific configuration, profile-conditional beans, and the right way to activate profiles in dev, CI, and production

← Back to Index

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.

No active profile = no profile-specific file loaded

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
create-drop in dev will surprise you exactly once

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
Never commit spring.profiles.active=prod to version control

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 accepts SpEL-style expressions for complex conditions

@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
The most common YAML multi-document bug: duplicate top-level keys in the same document

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 already replaces the DataSource โ€” application-test.properties may be redundant

@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

๐ŸŽ“ Junior level

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.

๐Ÿ”ฅ Senior level

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.