@Profile vs @ConditionalOnProperty in Spring Boot: How Production Systems Control Bean Creation
Spring Boot applications are not just about writing controllers, services, and repositories.
As applications grow, one very important question appears:
Should this bean be created in this environment?Should this feature be enabled right now?Should this implementation run in dev, test, staging, or production?
This is where annotations like:
@Profile
@ConditionalOnPropertybecome extremely useful.
Both annotations help Spring decide whether a bean should be registered in the application context or not.
But they solve different problems.
A simple way to remember is:
@Profile → Which environment am I running in?@ConditionalOnProperty → Is this feature/config enabled?Let’s understand both deeply with real-world examples.
The Core Idea: Conditional Bean Creation
In Spring Boot, classes annotated with annotations like:
@Component
@Service
@Repository
@RestController
@Configurationare candidates for bean creation.
During application startup, Spring scans these classes and creates beans inside the Spring container.
But sometimes we do not want every bean to be created every time.
For example:
In dev, use fake email service.In prod, use real email service.In local, log notifications to console.In production, send real notifications.Enable Redis cache only if redis.enabled=true.Enable a scheduler only if scheduler.enabled=true.
This is conditional bean creation.
Spring gives us multiple ways to do this, and two commonly used annotations are:
@Profile
@ConditionalOnPropertyWhat is @Profile?
@Profile is used when bean creation depends on the active environment.
Example environments:
dev
test
staging
prod
local
cloudSuppose we have a notification system.
In development, we do not want to send real emails.
We only want to print messages on the console.
public interface NotificationService {
void send(String message);
}Development implementation:
@Service
@Profile("dev")
public class ConsoleNotificationService implements NotificationService { @Override
public void send(String message) {
System.out.println("DEV notification: " + message);
}
}Production implementation:
@Service
@Profile("prod")
public class EmailNotificationService implements NotificationService { @Override
public void send(String message) {
// real email provider integration
System.out.println("Sending real email: " + message);
}
}Now if the active profile is:
spring.profiles.active=devSpring will create:
ConsoleNotificationServiceIf the active profile is:
spring.profiles.active=prodSpring will create:
EmailNotificationServiceThe consuming service does not need to know which implementation is active.
@Service
public class ExpenseService {private final NotificationService notificationService; public ExpenseService(NotificationService notificationService) {
this.notificationService = notificationService;
} public void createExpense() {
// save expense
notificationService.send("Expense created successfully");
}
}
This is clean design.
ExpenseService depends on the abstraction:
NotificationServiceSpring decides the actual implementation based on the active profile.
When Should We Use @Profile?
Use @Profile when the behavior changes based on environment.
Common real-world examples:
dev → mock payment gateway
prod → real payment gatewaytest → in-memory test database
prod → real production databaselocal → console notification
prod → email/SMS notificationdev → simple logging
prod → centralized logging integrationlocal → local file storage
prod → cloud object storage
So @Profile is mostly an environment-level decision.
Advanced @Profile Usage
Multiple Profiles
@Profile({"dev", "test"})This bean will be created if either dev or test profile is active.
Not Profile
@Profile("!prod")This bean will be created when the active profile is not prod.
Useful when you want something to run everywhere except production.
Profile Expressions
@Profile("dev | staging")This means the bean should be created for either dev or staging.
@Profile("prod & cloud")This means the bean should be created only when both prod and cloud profiles are active.
What is @ConditionalOnProperty?
@ConditionalOnProperty is used when bean creation depends on a property value.
It is part of Spring Boot’s conditional configuration mechanism.
Example:
@Service
@ConditionalOnProperty(
name = "notification.email.enabled",
havingValue = "true"
)
public class EmailNotificationService implements NotificationService { @Override
public void send(String message) {
System.out.println("Sending email: " + message);
}
}Property:
notification.email.enabled=trueIf the property is true, Spring creates the bean.
If the property is false, Spring skips the bean.
notification.email.enabled=falseIn that case, EmailNotificationService will not exist in the Spring container.
Understanding the Attributes of @ConditionalOnProperty
1. name
This defines the property key.
@ConditionalOnProperty(name = "cache.enabled")Spring checks:
cache.enabled=true2. prefix
This helps group properties neatly.
@ConditionalOnProperty(
prefix = "notification.email",
name = "enabled",
havingValue = "true"
)This checks:
notification.email.enabled=trueSo:
prefix + name = notification.email.enabled3. havingValue
This defines the expected value.
@ConditionalOnProperty(
name = "audit.enabled",
havingValue = "true"
)The bean will be created only when:
audit.enabled=true4. matchIfMissing
This controls what should happen if the property is missing.
@ConditionalOnProperty(
name = "audit.enabled",
havingValue = "true",
matchIfMissing = true
)This means:
If audit.enabled is missing, still create the bean.Use this carefully.
It is useful when you want a feature to be enabled by default.
Real Use Cases of @ConditionalOnProperty
@ConditionalOnProperty is very common in production systems.
It is useful for feature/config toggles.
Examples:
email.enabled=truesms.enabled=falseredis.cache.enabled=truekafka.consumer.enabled=truescheduler.enabled=falseaudit.logging.enabled=truemock.payment.enabled=falsenew.settlement.engine.enabled=true
Example: enabling or disabling a scheduler.
@Component
@ConditionalOnProperty(
name = "expense.reminder.scheduler.enabled",
havingValue = "true"
)
public class ExpenseReminderScheduler { @Scheduled(cron = "0 0 9 * * *")
public void sendReminders() {
System.out.println("Sending expense reminders...");
}
}Property:
expense.reminder.scheduler.enabled=trueIf this is true, the scheduler bean is created.
If false, the scheduler bean is not created.
This is useful because in production, sometimes scheduled jobs should run only in selected deployments or selected instances.
@Profile vs @ConditionalOnProperty
Both annotations control bean creation, but the decision basis is different.
AnnotationDecision Based OnBest For@ProfileActive environmentdev/test/staging/prod differences@ConditionalOnPropertyProperty valuefeature toggles and config switches
Simple example:
@Profile("prod")Means:
Create this bean only in production environment.@ConditionalOnProperty(
name = "notification.email.enabled",
havingValue = "true"
)Means:
Create this bean only when email notification is enabled.Can We Combine Both?
Yes.
In real systems, it is common to combine both.
@Service
@Profile("prod")
@ConditionalOnProperty(
name = "notification.email.enabled",
havingValue = "true"
)
public class EmailNotificationService implements NotificationService { @Override
public void send(String message) {
System.out.println("Sending production email: " + message);
}
}This bean will be created only when:
Active profile is prod
AND
notification.email.enabled=trueThis is useful when a feature should exist only in production and only when explicitly enabled.
What Happens Internally?
Both annotations are evaluated during application startup.
Get curiouswomen’s stories in your inbox
Join Medium for free to get updates from this writer.
The flow is roughly:
Spring Boot application starts
↓
Spring scans classes
↓
Finds @Component / @Service / @Configuration classes
↓
Checks conditions like @Profile and @ConditionalOnProperty
↓
If condition matches → bean is registered
↓
If condition does not match → bean is skippedImportant point:
If the condition does not match, the bean is not created at all.That means the bean will not be available for dependency injection.
Common Mistake 1: No Bean Available
Suppose we have this service:
@Service
public class ExpenseService {private final NotificationService notificationService; public ExpenseService(NotificationService notificationService) {
this.notificationService = notificationService;
}
}
If no implementation of NotificationService is active, Spring will fail during startup.
The error may look like:
No qualifying bean of type 'NotificationService' availableWhy?
Because Spring needs one bean to inject, but none was created.
Solution:
Ensure one implementation is always active.
Use profiles carefully.
Use properties carefully.
Provide a fallback implementation if needed.Example fallback:
@Service
@ConditionalOnProperty(
name = "notification.enabled",
havingValue = "false",
matchIfMissing = true
)
public class NoOpNotificationService implements NotificationService { @Override
public void send(String message) {
// intentionally do nothing
}
}A NoOp implementation is useful when the application should run even if notification is disabled.
Common Mistake 2: Multiple Beans Active
Suppose both these beans are active:
@Service
@Profile("dev")
public class ConsoleNotificationService implements NotificationService {
}@Service
@ConditionalOnProperty(
name = "notification.email.enabled",
havingValue = "true"
)
public class EmailNotificationService implements NotificationService {
}If active profile is dev and notification.email.enabled=true, Spring may create both beans.
Now when Spring sees:
public ExpenseService(NotificationService notificationService)It gets confused.
There are two NotificationService beans.
Which one should I inject?Possible solutions:
Use @Primary
Use @Qualifier
Make conditions mutually exclusive
Improve configuration designExample using @Qualifier:
public ExpenseService(
@Qualifier("emailNotificationService") NotificationService notificationService
) {
this.notificationService = notificationService;
}Common Mistake 3: Thinking It Changes Per Request
@Profile and @ConditionalOnProperty usually work during application startup.
They are not per-request switches.
This means:
One request cannot use dev bean and another request use prod bean.Once the application context is created, the beans are already decided.
If you change the property file after the app starts, the bean does not automatically disappear or appear unless you use a refresh mechanism or restart the application.
So:
@ConditionalOnProperty is mostly startup-time conditional configuration.For runtime user-based toggles, companies often use feature flag systems.
What Do Real Companies Use?
Yes, real companies use @Profile and @ConditionalOnProperty.
But they also use other configuration mechanisms depending on scale.
Common production configuration sources:
application-dev.yml
application-stage.yml
application-prod.yml
environment variables
Kubernetes ConfigMaps
Kubernetes Secrets
Spring Cloud Config
Vault
AWS Parameter Store
AWS Secrets Manager
feature flag systems@Profile in Companies
Used for environment-specific beans.
Example:
dev → mock implementation
prod → real implementation@ConditionalOnProperty in Companies
Used for property-driven toggles.
Example:
redis.enabled=true
kafka.consumer.enabled=true
scheduler.enabled=false
audit.enabled=trueFeature Flag Systems
For runtime feature rollout, companies may use feature flag tools.
Example use cases:
Enable new feature for 5% users
Enable feature only for internal employees
Roll out a new algorithm gradually
Disable risky feature instantly
Run A/B testingThis is different from @ConditionalOnProperty.
@ConditionalOnProperty usually decides bean creation at startup.
Feature flag systems can decide behavior dynamically at runtime.
Real-World Example: Payment Service
Suppose a system supports mock payment in dev and real payment in production.
Interface:
public interface PaymentService {
void pay(BigDecimal amount);
}Mock implementation:
@Service
@Profile("dev")
public class MockPaymentService implements PaymentService { @Override
public void pay(BigDecimal amount) {
System.out.println("Mock payment successful: " + amount);
}
}Real implementation:
@Service
@Profile("prod")
@ConditionalOnProperty(
name = "payment.gateway.enabled",
havingValue = "true"
)
public class RealPaymentService implements PaymentService { @Override
public void pay(BigDecimal amount) {
// call real payment gateway
}
}Fallback implementation:
@Service
@ConditionalOnProperty(
name = "payment.gateway.enabled",
havingValue = "false",
matchIfMissing = true
)
public class DisabledPaymentService implements PaymentService { @Override
public void pay(BigDecimal amount) {
throw new IllegalStateException("Payment gateway is disabled");
}
}This kind of setup gives control.
The application can behave differently across environments and configurations without changing business code everywhere.
Real-World Example: Splitwise-Like Notification System
In a Splitwise-like application, after an expense is created, we may want to notify group members.
In local development:
Print notification to console.In production:
Send real email or push notification.Interface:
public interface NotificationService {
void sendExpenseCreatedNotification(Long groupId);
}Development bean:
@Service
@Profile("dev")
public class ConsoleNotificationService implements NotificationService { @Override
public void sendExpenseCreatedNotification(Long groupId) {
System.out.println("Expense created for group: " + groupId);
}
}Production bean:
@Service
@Profile("prod")
@ConditionalOnProperty(
name = "notification.email.enabled",
havingValue = "true"
)
public class EmailNotificationService implements NotificationService { @Override
public void sendExpenseCreatedNotification(Long groupId) {
// send real email notification
}
}No-operation bean:
@Service
@ConditionalOnProperty(
name = "notification.email.enabled",
havingValue = "false",
matchIfMissing = true
)
public class NoOpNotificationService implements NotificationService { @Override
public void sendExpenseCreatedNotification(Long groupId) {
// do nothing
}
}Now the service remains clean:
@Service
public class ExpenseService {private final NotificationService notificationService; public ExpenseService(NotificationService notificationService) {
this.notificationService = notificationService;
} public void createExpense() {
// save expense
// save splits
notificationService.sendExpenseCreatedNotification(1L);
}
}
The service does not contain environment-specific if-else logic.
Spring handles the decision during startup.
Why Not Just Use if-else?
Without these annotations, code can become messy.
Example:
if (environment.equals("dev")) {
consoleNotification.send();
} else if (environment.equals("prod")) {
emailNotification.send();
}This spreads configuration logic inside business code.
Better approach:
Keep business code clean.
Move environment/config decision to Spring configuration.
Depend on interfaces.
Let Spring inject the correct implementation.That is the real value.
Best Practices
1. Use @Profile for environment-level decisions
Good:
dev vs prod
test vs real
local vs cloud2. Use @ConditionalOnProperty for feature-level decisions
Good:
email.enabled
redis.enabled
scheduler.enabled
audit.enabled
mock.gateway.enabled3. Avoid too many hidden conditions
If every bean has complex profile/property conditions, the system becomes hard to understand.
Configuration should be intentional and documented.
4. Ensure one required implementation exists
If a service needs NotificationService, make sure exactly one implementation is active.
5. Prefer constructor injection
Constructor injection makes required dependencies clear.
public ExpenseService(NotificationService notificationService) {
this.notificationService = notificationService;
}Avoid relying heavily on field injection with:
@Autowired(required = false)It may be okay for demos, but production code should make dependencies explicit.
6. Use feature flag tools for runtime rollout
If the requirement is:
Enable feature for 10% of users without restarting the app.Then @ConditionalOnProperty is usually not enough.
Use a feature flag system.
Interview-Level Explanation
A strong answer would be:
@Profile and @ConditionalOnProperty are both used for conditional bean registration in Spring.@Profile is environment-driven. It is useful when different environments like dev, test, staging, or prod need different beans.@ConditionalOnProperty is property-driven. It is useful for feature toggles or configuration-based bean creation, such as enabling Redis, Kafka consumers, email notifications, schedulers, or mock integrations.Both are evaluated during application context startup. If the condition does not match, the bean is not registered in the Spring container.In production systems, profiles and properties are often managed through application-{profile}.yml files, environment variables, Kubernetes ConfigMaps and Secrets, Spring Cloud Config, Vault, or cloud parameter stores. For runtime rollout or A/B testing, companies usually prefer feature flag systems.
Final Takeaway
@Profile and @ConditionalOnProperty are not just interview annotations.
They are practical tools for building configurable Spring Boot applications.
Remember:
@Profile
= Which environment?@ConditionalOnProperty
= Is this feature/config enabled?Feature flag system
= Can I control this dynamically at runtime?
A mature Spring Boot application should not hardcode every environment and feature decision inside business logic.
Instead:
Business code should depend on abstractions.Configuration should decide which implementation is active.Spring should create only the beans that are needed.
That is how these annotations help make applications cleaner, more flexible, and more production-ready.
Suggested Medium Titles
@Profilevs@ConditionalOnPropertyin Spring Boot: A Practical Production Guide- How Spring Boot Controls Bean Creation Using
@Profileand@ConditionalOnProperty - Conditional Bean Creation in Spring Boot: Profiles, Properties, and Production Use Cases
- Spring Boot Feature Toggles: Understanding
@Profileand@ConditionalOnProperty - From Dev to Prod: Using
@Profileand@ConditionalOnPropertythe Right Way
SEO Keywords
Spring Boot @Profile
Spring Boot @ConditionalOnProperty
Conditional Bean Creation Spring Boot
Spring Boot Profiles
Spring Boot Feature Toggle
Spring Boot Configuration
Spring Boot application.yml
Spring Boot Production Configuration
Spring Conditional Annotations
Spring Boot Bean RegistrationMedium Tags
Java
Spring Boot
Spring Framework
Backend Development
Software Engineering





