CodeGym /Courses /Spring Boot /Configuring catalog-service by profile

Configuring catalog-service by profile

Spring Boot
Level 15, Lesson 4
Available

1. The single-format rule

Profiles are useful, but tricky: they give you the feeling of “oh, now I can control everything,” and that is exactly why beginners often turn configuration into a set of copies of the same file with tiny differences, where after a week they are already afraid to change anything. This lecture is about discipline. We are not adding new Boot mechanics; we are cleaning up what we already know: one format, clear file responsibilities, minimal duplication, and a quick check of the final state.

When configuration is tidy, you gain predictability: you can easily answer questions like “why is the port different,” “why is more data visible in dev,” and “why does prod start differently.” When configuration is chaotic, guesswork begins: “it seems to be overridden somewhere... but where?” And yes, you can guess from the coffee grounds, but usually people do that on vacation, not in production.

If you have ever seen a project where application.yaml, application-dev.yaml, application.properties, application-prod.properties are all present at once, and somewhere else config.properties is hidden, then you know the feeling: “I am afraid to touch the config because I do not understand what will win.” In Spring Boot, you really can mix formats, but in a learning project (and in most small real services) this almost always makes things worse. Our goal is not to show “all options,” but to establish a normal baseline: YAML-first.

When .yaml and .properties sit next to each other

Spring Boot can read both .properties and .yaml. If both files are present in the same search location (for example, in src/main/resources), they will both be loaded, but the overlay order makes the behavior less obvious to a beginner. In practice, it looks like this: you edit YAML, expect the effect, but nothing changes — because there is a .properties file nearby that overrides the same key. A very “fun” bug, especially when you are sure you are editing the “main config.”

Mini anti-example:

# src/main/resources/application.yaml
# Base in YAML: set the value using the familiar YAML structure
app:
  catalog:
    # This key is easy to override from another source (and not notice)
    title: "Catalog From YAML"
# src/main/resources/application.properties
# Properties sits nearby and can "win" by priority
app.catalog.title=Catalog From Properties

If you then do this in code:

import org.springframework.core.env.Environment;

// Assume that Environment is already obtained from the Spring context
String title = environment.getProperty("app.catalog.title");
System.out.println(title); // Catalog From Properties (.properties won)

you will get the value from .properties. And then the typical student story begins: “Spring Boot ignores YAML” (no), “my caches are broken” (also not always), “the IDE did not pick it up” (the IDE is to blame, of course). In reality, it is simpler: two files for one key — two sources of truth.

Conclusion for the course and project

In catalog-service, we adopt a simple rule: inside the application, there is one primary configuration format — YAML. This is not a legal prohibition; it is engineering hygiene that drastically reduces the number of unexpected conflicts. If .properties files appear in the project later (for example, because of a third-party integration or an old template), that should be treated as an exception that you either remove, document, or deliberately migrate into YAML.

To make the rule about behavior, not just about nice words, it is worth doing this refactoring at least once: delete application.properties if it was created automatically, and keep only YAML files. Yes, sometimes this looks like “removing an extra file,” but in configuration an “extra file” is like an “extra wire” in an electrical panel: while it is not sparking, it seems fine, and then suddenly it is no joke.

2. Configuration file responsibilities

When profiles appear, it is important not only to “create application-local.yaml,” but also to agree on the role of each file. If the roles are not defined, the files will start growing like weeds: one profile will contain half of the shared values, another will contain the other half, and the base file will turn into an unclear “residue.” Our goal is to make configuration read like a document: shared part, then small adjustments for the environment.

A simple responsibility map

Below is a very practical table. You can literally print it and hang it on the wall next to your monitor so the profiles do not turn into a “copy-paste parallel universe.”

File What it stores How to read it
application.yaml Shared values and structure of app.catalog.* that do not depend on the environment “This is how the application is arranged overall”
application-local.yaml Only local differences for running on the developer’s machine “Convenient for me, but not for the whole team”
application-dev.yaml Shared dev baseline (general “non-prod” settings) “The team development environment”
application-prod.yaml Only differences for the production mode “Without local relaxations”

If you start wondering “where should I put this key,” try a simple question: should this value be the same in every environment? Then it belongs in application.yaml. Is this value tied specifically to my laptop (port, more “generous” defaults for viewing data)? Then it goes into application-local.yaml. Is this value common for the team’s dev environment? Then application-dev.yaml. Is this value a requirement for stricter behavior? Then application-prod.yaml.

Visual layer overlay diagram

Here is a simple diagram that helps you “feel” Boot behavior without mysticism:

flowchart TD
    A["application.yaml (base)"] --> B["application-local.yaml (override)"]
    A --> C["application-dev.yaml (override)"]
    A --> D["application-prod.yaml (override)"]

    B --> E["Final configuration (if local is active)"]
    C --> F["Final configuration (if dev is active)"]
    D --> G["Final configuration (if prod is active)"]

Important: a profile file does not “replace” the base config. It is added on top of it and overrides only matching keys. That is why the base file should be as complete as possible in structure, and the profile files should be short, like a good code comment: “here is the difference.”

3. Minimal duplication: “write the delta”

The most common mistake you will see in real projects (and often in your first projects too — yes, we have all been through this) looks like this: take application.yaml, copy it into application-dev.yaml, change two keys, and move on. A month later you change the base file, forget to update the copies, and end up with “why is dev one thing and prod another — although they should be the same.” That is why in this section we establish the style: a profile file contains only what really differs.

Bad style: a profile file as a “second version of the whole application”

Look at what a bad application-dev.yaml looks like: it almost completely repeats the base.

# Bad style: almost a full copy of the base (creates a second source of truth)
spring:
  application:
    name: "catalog-service"

app:
  catalog:
    title: "Course Catalog (dev)"
    max-featured-count: 4
    default-published-only: false
    startup-report-enabled: true
    maintenance-mode: false

The problem here is not that “this is forbidden.” The problem is that it creates two sources of truth. Now, if you want to change max-featured-count from 4 to 6 as a shared setting, you need to remember to change the base, dev, local, and prod too (if that is also a copy there). And if you forget, the application will behave “differently” for no obvious reason, and you will spend time investigating even though you created the cause yourself.

Good style: a profile file as a short “only differences” note

Here is the “good” version of the same application-dev.yaml, where only the differences remain.

# Good style: a short file, only differences relative to application.yaml
app:
  catalog:
    # In dev, we change only what we really want to differ from the base
    title: "Course Catalog (dev)"
    default-published-only: false

This file is small, but very informative. It literally says: “in dev, we want a different title and we want to show more than just published.” Everything else comes from the base file. This approach not only reduces mistakes, but also saves your energy: you read configuration with your eyes more often and do “archaeology” in the repository less often.

4. Final configuration of catalog-service

Now we need not just an example for understanding the mechanics, but a calm YAML-first baseline for catalog-service: one base application.yaml, short profile deltas, and no competing .properties nearby. Profile groups like full-dev can live nearby as a convenient alias for frequent launches, but the base set of files is fully usable even without them. On such a base, it is easy later to spread configuration across external files and additional locations without losing control over precedence.

application.yaml — base: structure and common defaults

Let us start with the base file. This is where the shared app.catalog.* structure lives, and it does not depend on the environment. Here you can also set file-based profile selection through a placeholder, if you need that for a learning project. The main thing is to keep profile selection in one clear place.

# src/main/resources/application.yaml
spring:
  application:
    # Application name: usually the same in all environments
    name: "catalog-service"
  profiles:
    # Profile selection from outside (env var), with a safe fallback to local
    active: "${APP_PROFILE:local}"

app:
  catalog:
    # Base title — the default for all profiles
    title: "Course Catalog"
    # Shared parameters that should not "float" between environments without a reason
    max-featured-count: 4
    default-published-only: true

Such a default does not conflict with older launch methods: SPRING_PROFILES_ACTIVE, -Dspring.profiles.active=..., and --spring.profiles.active=... can still override this choice for a specific start. And yes, app.catalog.* are our application-specific keys: Boot will honestly place them in Environment, and the application code gives them meaning.

application-local.yaml — local convenience without leaking into shared config

The local profile is your personal territory. Here it is fine to change the port (so it does not conflict with other applications), and here it is acceptable to make a more “convenient” default, for example by showing unpublished courses if you need to see the whole catalog during development.

# src/main/resources/application-local.yaml
server:
  # Local port: convenient, but should not end up in team dev/prod
  port: 8081

app:
  catalog:
    # In the local environment, we can explicitly mark that this is not dev/prod
    title: "Course Catalog (local)"
    # Locally, we allow seeing everything so development is easier
    default-published-only: false

Here the profile file is short: port and two differences. It does not rewrite max-featured-count, does not repeat spring.application.name, and does not copy flags that are already the same. It is like a well-written patch: it changes exactly what is needed.

application-dev.yaml — a shared dev baseline, but without personal details

The dev profile is not “my laptop,” but rather “the team’s hypothetical development environment.” Therefore, the port is usually not set here (or is set in a coordinated way), and the logic stays similar to local: you can show more data than in prod.

# src/main/resources/application-dev.yaml
app:
  catalog:
    # Dev — team environment: settings must be shared by everyone
    title: "Course Catalog (dev)"
    default-published-only: false

Notice that dev and local are similar, and that is normal. They solve similar tasks. The difference is that local can include “my” port and “my” conveniences, while dev is a shared baseline without individual differences.

application-prod.yaml — strict differences for production mode

If the base is already formulated as a safe default, the prod layer can be very short or may not appear at all until the first real production-specific difference. For the current state of catalog-service, an empty file is enough: the base values are already strict, and all relaxations live only in local and dev.

# src/main/resources/application-prod.yaml
# Still empty: for the current state of catalog-service
# safe defaults are already defined in application.yaml

If prod gets its own differences later, this is where that delta will go.

5. Checking the final picture

When you start working with profiles and multiple property sources, a new reality appears: configuration exists not only as a set of files, but also as the final state that the application actually sees through Environment. The technique is already familiar: look at which profiles and values actually won after the layers were applied. Now we will just wrap this diagnosis in a small ConfigSnapshot, so the project has one tidy observation point instead of a scattering of println calls in different places.

This is useful not only now. If part of the configuration later moves to external files, Environment will still show the final picture, rather than forcing you to remember from which exact layer the value came.

The ConfigSnapshot component: one line of truth for startup

Let us create a simple component. It does not try to become a “configuration system”; it just builds a short string: title and active profiles. That is often enough to quickly understand why the application behaves differently.

import org.springframework.core.env.Environment;
import org.springframework.stereotype.Component;

@Component
public class ConfigSnapshot {
    // Environment — a single point for reading final properties after profile overlay
    private final Environment environment;

    public ConfigSnapshot(Environment environment) {
        this.environment = environment;
    }

    public String summary() {
        // Grab the key property that often changes by profile
        String title = environment.getProperty("app.catalog.title", "n/a");

        // Active profiles — what Boot actually activated (not what we "expected")
        String profiles = String.join(", ", environment.getActiveProfiles());

        // If there are no profiles, we consider that we are running in the "base" mode
        return title + " | profiles=" + (profiles.isBlank() ? "base" : profiles);
    }
}

The important thing here is that we do not use any future type-safe binding mechanisms. We work with exactly what we already have: Environment and getProperty(...).

Hooking ConfigSnapshot into StartupSummaryRunner

Assume that StartupSummaryRunner already exists in your project (we introduced runners earlier). Now let us make it a bit more useful: instead of “just print the profiles,” print a short snapshot.

import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;

@Component
public class StartupSummaryRunner implements ApplicationRunner {
    // Inject the dependency that knows how to assemble the "final string"
    private final ConfigSnapshot snapshot;

    public StartupSummaryRunner(ConfigSnapshot snapshot) {
        this.snapshot = snapshot;
    }

    @Override
    public void run(ApplicationArguments args) {
        // Print one clear marker: you can immediately see both the title and the active profile
        System.out.println(snapshot.summary()); // Course Catalog (local) | profiles=local
    }
}

If you start the application with APP_PROFILE=dev, you will see a different text, and that instantly confirms that the profile was really activated and the properties were really overridden. It is a small thing, but it saves time and nerves. And time and nerves, as we know, are the two main non-renewable resources in IT.

6. Typical mistakes when working with profiles

Mistake #1: keeping application.yaml and application.properties as “two equal main files.”
This usually happens accidentally: one file comes from a template, and the other is added by a developer “because that is what they are used to.” Then a fight over priorities begins: you change YAML, but the changes do not show up because there is a .properties file with the same key nearby. If you chose a YAML-first approach, it is better to delete .properties and stop living in “guess which file won” mode.

Mistake #2: copying the entire base config into application-local.yaml / application-dev.yaml / application-prod.yaml.
This feels convenient exactly once — at the moment of copying. Then maintenance begins: you change the base, forget to change the copies, and get differences where you did not plan them. Profile files should be short, and a good guideline here is simple: if the profile file becomes larger than the base file, something went wrong.

Mistake #3: “local conveniences” leaking into the base application.yaml.
A classic example: you once bumped the port to 8081 to avoid a conflict with another service and put that in the base file. Later a colleague runs the project and suddenly gets another port, although they expected the default one. Local things (port, “show everything,” temporary flags) should live in application-local.yaml; otherwise you turn the base into a collection of personal preferences.

Mistake #4: setting spring.profiles.active inside application-dev.yaml or application-prod.yaml.
Such a config looks like an attempt at self-executing magic: “let the dev file enable dev itself.” But the logic breaks here: for Boot to apply application-dev.yaml, the dev profile must already be active. That is why the active profile is chosen either in the external launch (env vars / CLI args) or in one clear place in the base file through a placeholder, but not inside profile files.

Mistake #5: not checking the final state and hoping that “it is obvious anyway.”
When there are several profiles and several sources, expectations often diverge from reality. You are sure that dev is active, but in fact the application started with local from the fallback. You are sure that the title comes from the dev file, but it was overridden somewhere else. A small ConfigSnapshot or even a simple environment.getActiveProfiles() printout at startup is a way to stop “believing” and start “knowing.”

1
Task
Spring Boot, level 15, lesson 4
Locked
YAML-only configuration with `ConfigSnapshot`
YAML-only configuration with `ConfigSnapshot`
1
Task
Spring Boot, level 15, lesson 4
Locked
Shared Value Is Stored Only in the Base YAML
Shared Value Is Stored Only in the Base YAML
1
Survey/quiz
Spring Profiles, level 15, lesson 4
Unavailable
Spring Profiles
Configuration for different environments
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION