Stage 26: select a catalog source with a profile

Choose an implementation at startup without changing the consuming class.

Open this stage in the online learning path

Before you start

Use JDK 21 or newer and Maven 3.9 or newer. Verify with java -version and mvn -version. Extract this project into its own folder; do not merge its files with another stage.

This lesson reuses the product catalog and Todo responsibilities from Stages 8–14. Only the feature described below is new. Read the ordered explanation before opening the complete files.

Active profile → One matching @Bean → CatalogSource → Lesson

What we change and why

  1. Keep one contract

    CatalogSource describes the one operation the lesson needs. The two @Bean methods provide alternate implementations, with no database required yet.

  2. Activate exactly one profile

    The JVM system property spring.profiles.active selects dev or prod before context refresh. @Profile controls whether each factory method contributes its bean definition.

  3. Observe a missing or ambiguous choice

    With neither profile active, Lesson cannot receive CatalogSource. With both active, two candidates exist. Select one profile for this example. Profiles can also select configuration classes and components.

  4. Connect profiles to properties

    Boot also supports application-dev.properties and application-prod.properties for profile-specific values. A profile is a configuration selector, not an authorization or security boundary.

Run this example

Open a terminal in the extracted folder containing pom.xml. Run one command at a time.

Terminal · run dev and prod separately
mvn test
mvn spring-boot:run "-Dspring-boot.run.jvmArguments=-Dspring.profiles.active=dev"
mvn spring-boot:run "-Dspring-boot.run.jvmArguments=-Dspring.profiles.active=prod"

This is a console lesson. It does not open a web port. Application.main starts a Spring context, calls the demonstration and closes the context. Read the terminal, not a browser. Spring Boot’s Maven plugin is used as a convenient launcher; these examples show Spring Framework directly.

Expected result

Expected output · ignore framework startup messages
With dev: Sample products
With prod: Production catalog selected

Object identifiers and elapsed times can vary. Compare the relationship between values rather than copying an identifier.

Understand each complete file

Create the following files at exactly these paths, or use the ZIP. Source files include their imports. The test checks the behaviour described in the lesson.

pom.xml
pom.xml · complete file
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>4.1.1</version><relativePath/></parent>
  <groupId>com.edupoly</groupId><artifactId>spring-stage-26-profiles</artifactId><version>1.0.0</version>
  <properties><java.version>21</java.version></properties>
  <dependencies>
    <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter</artifactId></dependency>
    <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency>
  </dependencies>
  <build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build>
</project>
src/main/java/com/edupoly/lesson/AppConfig.java
src/main/java/com/edupoly/lesson/AppConfig.java · complete file
package com.edupoly.lesson;

import org.springframework.context.annotation.*;
@Configuration @Import(Lesson.class)
public class AppConfig {
    @Bean @Profile("dev") public CatalogSource developmentSource() { return () -> "Sample products"; }
    @Bean @Profile("prod") public CatalogSource productionSource() { return () -> "Production catalog selected"; }
}
src/main/java/com/edupoly/lesson/CatalogSource.java
src/main/java/com/edupoly/lesson/CatalogSource.java · complete file
package com.edupoly.lesson;

public interface CatalogSource { String description(); }
src/main/java/com/edupoly/lesson/Lesson.java
src/main/java/com/edupoly/lesson/Lesson.java · complete file
package com.edupoly.lesson;

public class Lesson {
    private final CatalogSource source;
    public Lesson(CatalogSource source) { this.source = source; }
    public void run() { System.out.println(source.description()); }
}
src/main/java/com/edupoly/lesson/Application.java
src/main/java/com/edupoly/lesson/Application.java · complete file
package com.edupoly.lesson;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class Application {
    public static void main(String[] args) throws Exception {
        try (var context = new AnnotationConfigApplicationContext(AppConfig.class)) {
            context.getBean(Lesson.class).run();
        }
    }
}
src/test/java/com/edupoly/lesson/LessonTest.java
src/test/java/com/edupoly/lesson/LessonTest.java · complete file
package com.edupoly.lesson;

import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

class LessonTest {
    @Test void eachProfileSelectsOneImplementation() {
        for (String profile : new String[]{"dev", "prod"}) {
            try (var context = new AnnotationConfigApplicationContext()) {
                context.getEnvironment().setActiveProfiles(profile); context.register(AppConfig.class); context.refresh();
                assertEquals(1, context.getBeansOfType(CatalogSource.class).size());
                assertEquals(profile.equals("dev") ? "Sample products" : "Production catalog selected", context.getBean(CatalogSource.class).description());
            }
        }
    }
}

Try a failure, then correct it

Run with neither profile. Read the missing CatalogSource dependency error. Then activate dev. Do not activate both dev and prod together in this example.

Words you can now explain

WordMeaning in this example
@ProfileConditionally registers a bean for an active environment profile.
Active profileThe startup selection dev or prod.

Practice assignment

Add a test profile returning Test products, activate it and verify the output.

Read the official documentation →