Mastering Spring Boot Configuration and Environments: @Profile, @PropertySource, and Conditional Annotations Explained

4 min read

Mastering Spring Boot Configuration and Environments: @Profile, @PropertySource, and Conditional Annotations Explained

1. Introduction

In modern Spring Boot applications, managing configurations and environments effectively is critical for building flexible, scalable, and maintainable systems. Spring provides a powerful set of annotations that help developers customize application behavior based on different conditions and environments. This article demonstrates how to use the following Spring annotations:

  • @PropertySource
  • @ConfigurationProperties
  • @Profile
  • @ConditionalOnProperty, @ConditionalOnClass, and @ConditionalOnMissingBean

Together, these annotations enable developers to externalize configuration, load environment-specific properties, and control bean creation conditionally.

2. The Problem

Applications often need to behave differently depending on the deployment environment — for example, using different databases, API endpoints, or caching strategies in dev, qa, or prod environments.

Without proper configuration management, developers may end up hardcoding values or writing complex conditional logic, making the code brittle and difficult to maintain. Additionally, when integrating third-party dependencies, we may need to load beans conditionally based on whether certain classes or properties are available.

3. The Solution

Spring Boot’s configuration and environment annotations solve this problem by providing:

  • Externalized configuration via .properties or .yml files.
  • Environment awareness through profiles.
  • Conditional bean loading using the @Conditional* family of annotations.

Using these annotations, we can build flexible configuration setups where application behavior is determined dynamically by environment and context — without modifying code.

4. Implementation

Let’s implement a small demo to understand how these annotations work together. We’ll build a sample Email Notification Service that behaves differently depending on the environment (dev or prod) and external configuration properties.

4.1. Project Structure

src/
 └── main/
     ├── java/com/example/configdemo/
     │   ├── Config/
     │   │   ├── AppConfig.java
     │   │   ├── EmailConfig.java
     │   │   └── NotificationAutoConfig.java
     │   ├── service/
     │   │   ├── EmailService.java
     │   │   └── SmtpEmailService.java
     │   │   └── MockEmailService.java
     │   └── ConfigDemoApplication.java
     └── resources/
         ├── application.properties
         ├── application-dev.properties
         └── application-prod.properties

4.2. Using @PropertySource and @ConfigurationProperties

These annotations allow you to load and bind external configuration properties into POJOs.

// File: EmailConfig.java
package com.example.configdemo.config;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;

@Configuration
@PropertySource("classpath:email.properties")
@ConfigurationProperties(prefix = "email")
public class EmailConfig {
    private String host;
    private int port;
    private String from;

    // Getters and Setters
    public String getHost() { return host; }
    public void setHost(String host) { this.host = host; }
    public int getPort() { return port; }
    public void setPort(int port) { this.port = port; }
    public String getFrom() { return from; }
    public void setFrom(String from) { this.from = from; }

    @Override
    public String toString() {
        return "EmailConfig{host='%s', port=%d, from='%s'}".formatted(host, port, from);
    }
}

email.properties

email.host=smtp.example.com
email.port=587
email.from=noreply@example.com

This setup loads email.properties and binds it to EmailConfig.

4.3. Using @Profile

With @Profile, we can define which beans should be active in a specific environment.

// File: EmailService.java
package com.example.configdemo.service;

public interface EmailService {
    void sendEmail(String to, String subject, String body);
}

// File: SmtpEmailService.java
package com.example.configdemo.service;

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

@Service
@Profile("prod")
public class SmtpEmailService implements EmailService {
    @Override
    public void sendEmail(String to, String subject, String body) {
        System.out.println("Sending real email via SMTP to " + to);
    }
}

// File: MockEmailService.java
package com.example.configdemo.service;

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

@Service
@Profile("dev")
public class MockEmailService implements EmailService {
    @Override
    public void sendEmail(String to, String subject, String body) {
        System.out.println("Mock email sent to " + to + " [DEV mode]");
    }
}

In application.properties, you can set:

spring.profiles.active=dev

or override it at runtime with:

-Dspring.profiles.active=prod

4.4. Using Conditional Annotations

Let’s automatically configure beans only when certain conditions are met.

// File: NotificationAutoConfig.java
package com.example.configdemo.config;

import com.example.configdemo.service.EmailService;
import com.example.configdemo.service.MockEmailService;
import org.springframework.boot.autoconfigure.condition.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class NotificationAutoConfig {

    @Bean
    @ConditionalOnProperty(name = "feature.notifications.enabled", havingValue = "true", matchIfMissing = false)
    public String notificationFeatureEnabled() {
        System.out.println("Notification feature is enabled!");
        return "FeatureEnabled";
    }

    @Bean
    @ConditionalOnClass(name = "com.example.configdemo.service.EmailService")
    public String emailServicePresent() {
        System.out.println("EmailService class is present in classpath.");
        return "EmailServiceAvailable";
    }

    @Bean
    @ConditionalOnMissingBean(EmailService.class)
    public EmailService defaultEmailService() {
        System.out.println("No EmailService bean found — using default mock.");
        return new MockEmailService();
    }
}

In application.properties:

feature.notifications.enabled=true

4.5. Application Entry Point

// File: ConfigDemoApplication.java
package com.example.configdemo;

import com.example.configdemo.service.EmailService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class ConfigDemoApplication {

    @Autowired
    private EmailService emailService;

    public static void main(String[] args) {
        SpringApplication.run(ConfigDemoApplication.class, args);
    }

    // Example usage
    @PostConstruct
    public void testEmailService() {
        emailService.sendEmail("user@example.com", "Welcome", "Hello from Config Demo!");
    }
}

5. Conclusion

Spring’s configuration and environment annotations make it easy to manage application behavior dynamically without cluttering code with if-else logic or hardcoded values.

  • @PropertySource and @ConfigurationProperties externalize and map configurations.
  • @Profile activates environment-specific beans.
  • Conditional annotations like @ConditionalOnProperty, @ConditionalOnClass, and @ConditionalOnMissingBean help in building modular, context-aware applications.

By using these annotations strategically, you can create cleaner, more maintainable, and environment-resilient Spring Boot applications.

🤞 Never miss a story from us, get weekly updates to your inbox!

Leave a Reply

Your email address will not be published. Required fields are marked *