Sitemap

@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
@ConditionalOnProperty

become 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
@Configuration

are 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
@ConditionalOnProperty

What is @Profile?

@Profile is used when bean creation depends on the active environment.

Example environments:

dev
test
staging
prod
local
cloud

Suppose 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=dev

Spring will create:

ConsoleNotificationService

If the active profile is:

spring.profiles.active=prod

Spring will create:

EmailNotificationService

The 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:

NotificationService

Spring 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 gateway
test → in-memory test database
prod → real production database
local → console notification
prod → email/SMS notification
dev → simple logging
prod → centralized logging integration
local → 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=true

If the property is true, Spring creates the bean.

If the property is false, Spring skips the bean.

notification.email.enabled=false

In 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=true

2. prefix

This helps group properties neatly.

@ConditionalOnProperty(
prefix = "notification.email",
name = "enabled",
havingValue = "true"
)

This checks:

notification.email.enabled=true

So:

prefix + name = notification.email.enabled

3. havingValue

This defines the expected value.

@ConditionalOnProperty(
name = "audit.enabled",
havingValue = "true"
)

The bean will be created only when:

audit.enabled=true

4. 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=true
sms.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=true

If 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=true

This 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 skipped

Important 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' available

Why?

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 design

Example 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=true

Feature 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 testing

This 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 cloud

2. Use @ConditionalOnProperty for feature-level decisions

Good:

email.enabled
redis.enabled
scheduler.enabled
audit.enabled
mock.gateway.enabled

3. 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

  1. @Profile vs @ConditionalOnProperty in Spring Boot: A Practical Production Guide
  2. How Spring Boot Controls Bean Creation Using @Profile and @ConditionalOnProperty
  3. Conditional Bean Creation in Spring Boot: Profiles, Properties, and Production Use Cases
  4. Spring Boot Feature Toggles: Understanding @Profile and @ConditionalOnProperty
  5. From Dev to Prod: Using @Profile and @ConditionalOnProperty the 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 Registration

Medium Tags

Java
Spring Boot
Spring Framework
Backend Development
Software Engineering

--

--

curiouswomen
curiouswomen

Written by curiouswomen

I am here to share my experience being it a technical, non-technical or daily life experience that I am curious and thrive a positive change.