CodeGym /Courses /Spring Core /Spring profiles: active/ default/ expr

Spring profiles: active/ default/ expr

Spring Core
Level 14, Lesson 2
Available

1. Profiles and the refresh() moment

If you have ever tried to “change horses midstream” and enable a profile after the context started, you already know the feeling: the profile looks set, but the beans are still exactly where they were. The important thing here is to catch the right picture. Profiles affect registration and filtering of BeanDefinition, and that happens during refresh() (or inside the context constructor that calls refresh() automatically). After startup, the container is not required to rebuild itself from scratch — it is not a LEGO set you can just summon; it is more like an “assembled robot” that has already started running.

To see this moment in action, it helps to keep a very short startup diagram for AnnotationConfigApplicationContext in mind:

flowchart TD
    A["Created the context"] --> B["Configured Environment (profiles, properties)"]
    B --> C["Registered configuration (register)"]
    C --> D["refresh()"]
    D --> E["Filtering @Profile and registering BeanDefinitions"]
    E --> F["Creating singleton beans"]
    F --> G["Context is ready"]

The key idea here is simple: profiles must be defined before the refresh() step. If refresh() has already run, your @Profile("demo")-beans have either already made it into the context or have already been thrown out — and you cannot “turn them back on” with a single setter.

Below is a small example of a typical mistake. It is especially tricky because at first glance it looks logical: “well, I am just setting the profile in the environment.”

import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class WrongProfileTiming {
    public static void main(String[] args) {
        // IMPORTANT: this constructor starts the context itself and calls refresh() inside
        try (AnnotationConfigApplicationContext ctx =
                     new AnnotationConfigApplicationContext(AppConfig.class)) {

            // Mistake: the profile is changed too late — BeanDefinitions have already been filtered by @Profile
            ctx.getEnvironment().setActiveProfiles("demo");
            // Check whether the bean that should exist only in the demo profile is present
            System.out.println(ctx.containsBean("fileAuditWriter")); // false
        }
    }
}

The comment on this code sounds almost like a diagnosis: AnnotationConfigApplicationContext(AppConfig.class) has already started the context, and inside the constructor refresh() was executed. Yes, you changed the Environment inside the object, but @Profile filtering had already happened earlier. As a result, fileAuditWriter (which you registered, say, in the demo profile) will never appear, even if you later say “profile demo, really and truly” a hundred times.

2. Activating profiles

Programmatically via setActiveProfiles(...)

When we are learning, the clearest path is to enable a profile from code. This is not a “forever production strategy,” but it is a great way to make sure you understand the mechanics. In plain Spring (without Boot), the most direct approach is to create the context empty, set the active profile in the Environment, register the configuration, and only then call refresh(). In other words, we literally do by hand what Boot will later do “for us.”

Here is a working minimal template. Pay attention: we use an empty constructor and call refresh() explicitly.

import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class ProfiledStartup {
    public static void main(String[] args) {
        // Create the context "empty": it is not refreshed yet, and the beans are not filtered yet
        try (AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext()) {
            // First set the active profile (this must be done BEFORE refresh)
            ctx.getEnvironment().setActiveProfiles("demo");
            // Register the configuration (@Configuration / @ComponentScan classes)
            ctx.register(AppConfig.class);
            // Now run refresh() — this is where @Profile filtering is applied
            ctx.refresh();

            // Check that the bean from demo really made it into the context
            System.out.println(ctx.containsBean("fileAuditWriter")); // true (in demo)
        }
    }
}

In this example, everything happens in the “right” order. The profile is set before refresh(), so @Profile("demo") really does affect which BeanDefinition end up in the container.

It is also useful to remember that setActiveProfiles(...) accepts multiple values. Spring can do that, but for ContextFlow on this day it is better to keep a single top-level mode per run: dev or demo or test. That keeps the build readable instead of turning it into combinatorics with random combinations.

Via the property: spring.profiles.active

Profiles are an environment thing, and the environment is usually something you want to set from the outside, rather than editing code and recompiling the application. Spring understands this and therefore can read active profiles from the Environment using the spring.profiles.active key. This is convenient for us: we can run the same application in different modes by changing only a VM option or an environment variable. And yes, this works in plain Spring too, not only in Spring Boot.

From a practical point of view, there are several “entry points” from which a value can reach the Environment. It is convenient to see them in a small table:

Method Example When it is convenient
JVM system property -Dspring.profiles.active=demo local launch from an IDE, scripts, Gradle run
Programmatically via System.setProperty(...) System.setProperty("spring.profiles.active", "dev"); learning examples and demos (but be careful)
Environment variable SPRING_PROFILES_ACTIVE=demo CI runs, containers, OS “external” config

The most “honest” demo is to set the system property before creating the context. Then you can even use the “convenient” AnnotationConfigApplicationContext(AppConfig.class) constructor, because the value will already be available while it runs:

import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class PropertyDrivenProfileStartup {
    public static void main(String[] args) {
        // IMPORTANT: set the system property before creating the context,
        // so it reaches the Environment before refresh()
        System.setProperty("spring.profiles.active", "dev");

        // This constructor will call refresh() itself, and the profile will already be visible to the container
        try (AnnotationConfigApplicationContext ctx =
                     new AnnotationConfigApplicationContext(AppConfig.class)) {

            // In this example we keep one top-level mode, so we can safely print the first element
            System.out.println(ctx.getEnvironment().getActiveProfiles()[0]); // dev
        }
    }
}

There is an important nuance here: System.setProperty(...) changes the state of the entire JVM. In a small console app that is acceptable, but in tests or in a complex startup you can get the amusing “the first test enabled demo — the others are suddenly demo too” effect. So as soon as you feel you have more than one startup scenario, the programmatic setActiveProfiles(...) in a local context becomes safer and more predictable than the global System.setProperty(...).

3. The default profile

Now let us look at something that often confuses beginners: the default profile. It does not mean “the production profile” and it does not mean “the default mode that is always with everything else.” In meaning, it is more like a “backup parachute”: it is used only if no active profiles are defined at all. As soon as you set at least one active profile, default stops taking part in the game.

This is easy to verify with a very short code fragment. It shows something many people do not expect: there can be zero active profiles, yet the default profiles still exist.

import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import java.util.Arrays;

public class DefaultProfilesDemo {
    public static void main(String[] args) {
        // Create the context without profiles and without configuration: just inspect what the Environment contains by default
        try (AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext()) {
            // Active profiles are empty because we did not set anything
            System.out.println(Arrays.toString(ctx.getEnvironment().getActiveProfiles()));  // []
            // Default profiles always exist unless you explicitly override them
            System.out.println(Arrays.toString(ctx.getEnvironment().getDefaultProfiles())); // [default]
        }
    }
}

So “active profiles” and “default profiles” are two different things. The mechanism is roughly this: if active profiles are not set, Spring uses default when checking @Profile.

Then why do we need spring.profiles.default? To rename or replace the set of profiles that Spring will consider “default” if no active profile is selected. For example, if you want a local startup without extra settings to automatically become dev, you can do this:

import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class CustomDefaultProfileStartup {
    public static void main(String[] args) {
        // Override the default-profile set at the JVM level (for demo purposes)
        System.setProperty("spring.profiles.default", "dev");

        // Create the context with the "convenient" constructor: it will start immediately,
        // and the container will see the default profile during refresh()
        try (AnnotationConfigApplicationContext ctx =
                     new AnnotationConfigApplicationContext(AppConfig.class)) {

            // Check that the dev profile is considered suitable by the matching logic
            System.out.println(ctx.getEnvironment().matchesProfiles("dev")); // true
        }
    }
}

And again, this is a solution “about the environment.” In real life you would more likely set SPRING_PROFILES_DEFAULT=dev or a VM option than hardcode it in main(). But for understanding the mechanics, this example works great: you can see that the “default” profile lives separately from the “active” one, and is enabled only when no active profile exists.

One more nuance is worth stating plainly. The @Profile("default") annotation means: “this bean will be registered if no active profiles are set and the default profiles include default.” This is a good way to keep a “base” configuration without special modes, but do not turn default into “just another normal profile” on the same level as dev/demo/test. For a project, it is much clearer when you explicitly launch dev than when you try to guess what is inside default.

4. Profile expressions

Profiles quickly run into an everyday task: “this bean is needed everywhere except tests” or “this bean is needed in dev or demo.” If you do this only by listing profiles, you can start duplicating things. That is why Spring supports profile expressions — small logical expressions inside @Profile, for example !test. This is not magic and not a separate programming language, just compact selection logic.

To avoid turning the topic into rote memorisation, it is enough to remember three operators and parentheses. I deliberately put them in a table, because the beginner brain likes tables more than long paragraphs (and I understand it):

Operator Meaning Example
! NOT !test — “everything except test”
& AND demo & debug — “only when both are enabled”
| OR dev | demo — “when at least one is enabled”

Spring can match more complex combinations too, but for ContextFlow the top-level mode still remains one: dev or demo or test. Expressions here are mainly useful for targeted conditions like !test, not for freely mixing all three modes.

The most common and most useful example for our ContextFlow: in test mode we want to disable real side effects, for example “real” notifications. Then the bean responsible for “live” notifications can be marked like this:

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

// Means: this component will be created in all profiles EXCEPT test
@Profile("!test")
@Component
class ConsoleNotificationSender implements NotificationSender {
    @Override
    public void send(String message) {
        // Here we intentionally keep a "live" action: output/notification,
        // which tests usually want to disable
        System.out.println("NOTIFY: " + message); // NOTIFY: ...
    }
}

It looks straightforward: tests are tests, notifications are not needed there. In all other modes — sure.

Another example is “dev or demo.” It is often written either as @Profile({"dev", "demo"}), or as the expression @Profile("dev | demo"). For a beginner, the first option is usually more readable because it has fewer symbols that look like “high school math.” But the expression is useful when you want to add negation or combinations.

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;

@Configuration
class NonTestAuditConfig {
    @Bean
    // Expression: the bean will be registered if dev OR demo is active
    @Profile("dev | demo")
    AuditWriter consoleAuditWriter() {
        // Return the concrete implementation we want to enable only in these modes
        return new ConsoleAuditWriter();
    }
}

It is important not to fool yourself here: a profile expression is still evaluated when the container starts, not “on every method call.” In other words, this is not runtime branching; it is still conditional registration. If you enabled the profile, the bean either will be there or will not be there. No “today I am dev, and in a minute I am demo” inside one context. Thank goodness, otherwise debugging would turn into horror.

5. Printing active and default profiles

Once profiles start living in configuration, you naturally want quick self-checking: “did I really start in the mode I think I did?” This is especially useful in a learning project, where you often switch between dev/demo/test and want to see that the container actually assembled different implementations. The simplest way is to print the profiles in main() after refresh().

Below is a minimal example for starting ContextFlow with a profile from the command-line arguments. It does not turn the project into a CLI monster, but it gives honest control over the build.

import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class ContextFlowMain {
    public static void main(String[] args) {
        // Take the profile from the command-line arguments; otherwise use dev as a "clear default" for learning startup
        String profile = (args.length > 0) ? args[0] : "dev";

        // Create the context empty so we have time to set the profile BEFORE refresh()
        try (AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext()) {
            // The profile must be set before registration/refresh so that @Profile has time to filter beans
            ctx.getEnvironment().setActiveProfiles(profile);
            // Register application configuration
            ctx.register(AppConfig.class);
            // Start the context
            ctx.refresh();

            // Quick self-check: what mode did we actually run in?
            System.out.println("Active profile = " + profile); // Active profile = dev
            // Run the scenario that uses the assembled set of beans
            ctx.getBean(ScenarioRunner.class).run();
        }
    }
}

Notice one methodological point: we print what we chose, rather than trying to “guess” from indirect signs. Yes, you can output ctx.getEnvironment().getActiveProfiles(), but in learning code it is sometimes even easier to print the profile variable so the eye catches it immediately. When the application gets larger, of course, you will log this more carefully, but right now we are simply making container behaviour observable.

If you want something a bit more formal, you can print both arrays — active and default. Just do not panic when active is empty: that is normal if you start without an explicit profile and rely on default.

import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import java.util.Arrays;

public class ProfilesPrint {
    public static void main(String[] args) {
        // Here we use the "convenient" constructor — it will start the context immediately
        try (AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class)) {
            // Active profiles: what was explicitly enabled (may be empty)
            System.out.println(Arrays.toString(ctx.getEnvironment().getActiveProfiles()));  // []
            // Default profiles: fallback set used if active is empty
            System.out.println(Arrays.toString(ctx.getEnvironment().getDefaultProfiles())); // [default]
        }
    }
}

6. Typical mistakes when working with profiles

Mistake #1: enabling a profile after refresh() and expecting the context to “rebuild itself.”
This is the most common trap. A profile affects bean registration, and registration happens before objects are created. Once the context is up, changing the profile in the Environment is the same as changing the house plan after you have already built it. You could tear it down and build it again, of course, but then it is a different context, not a “mode switch.”

Mistake #2: thinking that default works at the same time as any active profile.
The default mechanism is fallback in the absence of active profiles. As soon as you set dev, demo, or test, the default profile stops being considered during @Profile checks. Because of that, a bean with @Profile("default") suddenly disappears, and the beginner thinks: “Spring is broken.” No, it is just designed that way.

Mistake #3: setting System.setProperty("spring.profiles.active"...) in tests and forgetting to clean up.
System properties are global to the JVM. One test sets demo, another test expected test or an empty active profile — and you get flaky tests without a single line of multithreading. In learning examples, System.setProperty is acceptable, but in real life it is better to manage profiles through the specific context Environment or through the test context startup settings.

Mistake #4: turning profiles into “settings for every tiny thing.”
A profile should describe the application mode, not the value of one number. If you created profiles limit-10, limit-20, limit-30, then you built a system that cannot be maintained: there are too many combinations, and the meaning of profiles becomes blurry. For numbers and strings we have properties; profiles are for selecting a set of implementations.

Mistake #5: writing profile expressions that only the author and their cat understand (and not always even then).
@Profile("dev & (demo | !test) & (blue | green)") looks clever right up to the first code review. Profile expressions are useful, but it is better to keep them simple: !test, dev | demo, at most something like demo & debug. If the expression is more complex, you usually gain more by splitting the configuration into clear modules and explicit profiles.

1
Task
Spring Core, level 14, lesson 2
Locked
Default profile via `spring.profiles.default`
Default profile via `spring.profiles.default`
1
Task
Spring Core, level 14, lesson 2
Locked
Profile expression `!test` and activation through `spring.profiles.active`
Profile expression `!test` and activation through `spring.profiles.active`
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION