The problem profiles solve
Your application runs in more than one place. It runs on your laptop while you build it. It runs on a continuous-integration server that executes the tests. It runs in staging, where the team clicks around before a release. And it runs in production, serving real users.
The code is identical in all four. What changes is the configuration around it: the database it points at, how much it logs, which external services are real versus faked. On your laptop the database might be a throwaway one that lives in memory. In production it is a real, carefully guarded server.
So you need one build that behaves differently depending on where it wakes up. That is exactly what a profile is: a named set of configuration that Spring switches on or off as a group. Name the group dev, prod, or test; turn one on; the pieces tagged with that name come alive, and the rest stay dormant.
First, where configuration lives
Before profiles make sense, one Spring idea has to be clear: the container.
When a Spring application starts, it builds a big registry of the objects your app is made of — the object that talks to the database, the one that handles web requests, the one that sends email. Spring creates them, wires them together, and hands them out wherever they are needed. Each managed object is a bean, and the registry that holds them is the container.
Here is the key part: the container is assembled once, at startup. Spring decides which beans to create, and how to configure them, in those first moments — before a single request is served.
Profiles hook into exactly that decision. A profile can change which beans get created, and it can change the values they are configured with. Let's take those one at a time.
Tagging a bean with a profile
Say you want an in-memory database on your laptop but the real one in production. You describe both, and mark each with the environment it belongs to.
@Configuration
public class DataSourceConfig {
@Bean
@Profile("dev")
public DataSource devDataSource() {
// an in-memory database, wiped on every restart
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.H2)
.build();
}
@Bean
@Profile("prod")
public DataSource prodDataSource() {
// the real, persistent database
HikariDataSource ds = new HikariDataSource();
ds.setJdbcUrl("jdbc:postgresql://db.internal:5432/orders");
return ds;
}
}
Two annotations do the work. @Bean marks a method whose return value becomes a bean in the container. @Profile("dev") attaches a condition to it: only register this bean when the dev profile is active.
So on your laptop with dev active, the container builds the in-memory database and never even looks at the production one. Flip to prod and the opposite happens.
Notice what the rest of the app sees. Both methods produce a DataSource. The code that needs a database just asks for a DataSource and gets whichever one this environment built. The switch is invisible to everything downstream — which raises the obvious question: how does Spring know which profile is active?
Turning a profile on
A profile is just a string, and it stays inactive until something names it. That "something" is a property called spring.profiles.active.
You can set it several ways, and they all exist because the answer comes from a different place in each environment.
In a properties file bundled with the app:
spring.profiles.active=dev
As an environment variable, which is how a deployment platform usually does it:
export SPRING_PROFILES_ACTIVE=prod
Or on the command line when you launch the jar:
java -jar orders.jar --spring.profiles.active=prod
All three set the same underlying property. They differ only in who gets to decide, and a more specific source wins: the command line overrides the environment variable, which overrides the file baked into the jar. That ordering is what lets an ops team point the app at a different database without rebuilding anything.
You can also activate more than one profile at once, comma-separated:
spring.profiles.active=prod,metrics,cloud
Now three profiles are active together, and a bean tagged with any of them joins the container. This is how you compose a full environment out of smaller, reusable slices.
The property side: profile-specific files
Beans are one half of the story. The other half is plain configuration values — a URL, a timeout, a log level. For these, Spring Boot gives profiles a naming convention.
There is a base file, application.properties, that always loads. Next to it you place one file per profile: application-dev.properties, application-prod.properties, and so on.
# application.properties (shared by everyone)
app.name=Orders Service
app.page-size=20
# application-dev.properties (only when dev is active)
logging.level.root=DEBUG
app.page-size=5
# application-prod.properties (only when prod is active)
logging.level.root=WARN
Spring loads the base file first, then the file for each active profile on top of it. A key that appears in a profile file overrides the same key in the base. A key that appears only in the base is left untouched.
So with dev active, app.name stays "Orders Service" from the base, logging.level.root becomes DEBUG, and app.page-size becomes 5 — the dev file won the last two. A profile file is an override layer, not a replacement. You keep the shared defaults in one place and state only the differences per environment.
(If you prefer YAML, the same idea lives in a single application.yml split into sections with ---, each tagged by spring.config.activate.on-profile. Same layering, one file.)
The default profile
What if nothing sets spring.profiles.active? No named profile is active — but configuration still has to come from somewhere. Spring covers this with the default profile.
Any bean or property file not tagged with a profile belongs to the default and is always in play. On top of that, there is a fallback profile literally named default, and it is active precisely when no other profile is. So application-default.properties loads only when you launched with nothing set — a handy spot for sensible local values that a real environment overrides.
This catches people out: a bean tagged @Profile("default") disappears the moment you activate any other profile, even an unrelated one. "default" means "when nothing else," not "always."
Profile expressions
Beyond a single name, @Profile understands a small expression language built from ! (not), & (and), and | (or).
@Bean
@Profile("!prod")
public EmailSender loggingEmailSender() {
// writes the "email" to the log instead of sending it
return new LoggingEmailSender();
}
Here !prod means "every environment except production." Dev and staging get the harmless logging sender; production gets the real one, defined elsewhere with @Profile("prod"). You can combine terms too: @Profile("prod & cloud") needs both active, @Profile("dev | test") needs either. This lets you express a real condition without inventing a brand-new profile name for every combination.
Profile groups
Long comma-separated lists get repetitive and easy to get wrong. Spring Boot lets you name a bundle once, as a profile group.
spring.profiles.group.prod=metrics,cloud,audit
Now activating prod automatically activates metrics, cloud, and audit as well. One switch, four profiles. Your deployment config stays a single meaningful name — spring.profiles.active=prod — while the composition it expands to lives in one readable line.
Profiles in tests
A test run is just another environment, and it gets first-class support through @ActiveProfiles.
@SpringBootTest
@ActiveProfiles("test")
class OrderServiceTest {
// the container here is built with the "test" profile active
}
The container for this test is assembled with test active, so application-test.properties and any @Profile("test") beans come into play — an in-memory database, a stubbed payment gateway — without touching how the app boots in production.
The one trap worth internalising
Profiles decide the shape of the container at startup. That is the whole mechanism — and also its main limit.
You cannot switch a profile per request, per user, or at runtime. By the time your code runs, the beans are already chosen and the profile decision is frozen. If you catch yourself wanting to "turn on the prod behaviour for just this one call," profiles are the wrong tool; that is a runtime flag, not a profile.
There is a subtler trap as well. It is tempting to route every optional feature through @Profile, but each profiled bean is a fork in your wiring that appears in only one environment — and therefore gets tested in only one. When the real question is "is this feature on or off," Spring Boot's @ConditionalOnProperty (a bean that appears based on a single property's value) is usually clearer, because the app boots the same way everywhere and simply reads a flag. Reserve profiles for genuine, environment-shaped differences — the database, the external services, the logging posture — and you keep their power without turning startup into a maze.
Putting it together
A profile is a name. You attach that name to beans with @Profile, or to configuration values by dropping them in an application-<name>.properties file, and you switch it on with spring.profiles.active. When the container is assembled at startup, Spring keeps the pieces whose name is active and skips the rest.
That single idea — choose the environment once, at boot — is what lets one identical build behave correctly on your laptop, in your tests, and in production, without a single if (production) anywhere in your code.
Top comments (0)