1. Files application*.yml and env vars
When you start containerizing a Spring Boot service, it is very easy to fall into a trap: “Let’s just set everything with environment variables — that is so convenient!” And yes, setting one or two variables is nice. But as soon as you have 10–20 settings (port, profiles, datasource, export, logging, actuator), env vars turn into a long wall that nobody wants to read, let alone maintain.
This is where “proper adult” configuration appears: shared settings live in application.yml, and what differs between modes is split into short profile-specific files. This gives you readability, predictability, and a very important thing for Docker: the ability to change behaviour through profiles rather than by copying a Dockerfile or “building a new image for postgres mode”.
To make it easier to keep the picture in your head, imagine configuration not as one giant scroll, but as “layers of clothing”:
- the base layer — application.yml (what is always “on you”);
- on top of it — application-postgres.yml, application-standalone.yml, and so on (what you put on depending on the weather/situation).
With this approach there is less magic and more engineering: it is clear what exactly changes, and why.
2. The role of the base application.yml
The word “boring” in configuration is a compliment. A good application.yml usually does not try to surprise you; it tries to be predictable. It contains what suits practically any launch mode: the default port, the base settings of your application, values common to all profiles. The fewer “special cases” are in this file, the easier it is to explain how the project starts, both inside the team and in the course.
In our end-to-end project Container-Ready Catalog Service, there is an excellent candidate for a shared setting — the export directory. Exporting the catalog to a file is part of the project domain, and the application needs to know where to place CSV files. This is not “postgres-specific” and not “standalone-specific”. That means this is a base config.
We do not need the full project file right now, just a small fragment that shows which keys should be considered shared. It can live in src/main/resources/application.yml:
# src/main/resources/application.yml
server:
# Default port: the service starts "out of the box", and in Docker it can be overridden from the outside if needed
port: 8080
app:
# A path shared by all profiles where the service stores exports
export-dir: /app/exports
There are two important ideas here.
The first is that server.port is usually shared, because the port is “how the service talks to the world”. In a Docker context, the port can be overridden from the outside, but it is still convenient to have a base value so the application starts “out of the box”.
The second is that app.export-dir is also shared: the export feature works in all modes, and it should have a default value. Choosing /app/exports fits the container reality well: the application has a working directory, and it is enough to agree in advance that the default export location inside the container looks like this. We are not touching the file mechanics of this directory yet — what matters here is the role of the shared key.
To connect YAML with “real” Java code and remove the beginner’s feeling that these are just magic keys, we can show a very simple example of reading a property in a Spring component:
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@Component
public class ExportDirHolder {
// Pull the value from the Spring Environment by the key from application.yml
@Value("${app.export-dir}")
private String exportDir;
// Here there could be a getter/method that uses exportDir during export
}
Yes, this is deliberately simple. In a real project you can (and often should) do it more carefully, but right now we need to see the chain: a key in YAML → the value reaches the Spring Environment → it can be used in code.
If you keep application.yml short, it becomes something like the application’s “passport page”: you open it and immediately understand the base. And if it contains half the world, it turns into a “chaos reference manual”, and every new launch mode will only make that chaos bigger.
3. Profile-specific application-{profile}.yml
A profile file is not “just another application.yml, only for special cases”. It is a small overlay that contains exactly the keys that really differ in a specific mode. This is critical: if you copy the entire base config into application-postgres.yml, you create the problem yourself — now “shared values” live in two places, and in a month nobody will remember which one is more “correct”.
In our project there is a base standalone mode, where storage is in-memory. This is a great example of a profile difference: what changes is not the port, not export-dir, but the storage mode.
Let us take two minimal fragments, where only the differences between modes are visible, not the full future file versions. For standalone it may look like this:
# src/main/resources/application-standalone.yml
app:
# In standalone we use in-memory storage
storage-mode: memory
Notice how short it is. And that is good. It literally answers one question: “how is standalone different from the others?”. No copies of server.port, no duplicates of app.export-dir.
Now let us look at the postgres mode. Here the difference is in the data source: datasource, Flyway, JPA persistence are enabled. We are not going into JPA details here (that is a different course), but at the configuration level the key difference is the datasource settings.
Example:
# src/main/resources/application-postgres.yml
spring:
datasource:
# URL to postgres; postgres-host here is just a placeholder for the host/service name
url: jdbc:postgresql://postgres-host:5432/catalog
# Database credentials for the application
username: catalog
# Password: in real life it usually comes from secrets/env vars
password: catalog
Again — only the differences. We do not repeat server.port. We do not repeat app.export-dir. We do not pretend that this is a “second full application config”. It is just a layer that is applied on top of the base application.yml when the postgres profile is activated.
What matters here is the domain itself for spring.datasource.*, not the specific host in the example. The concrete value depends on the runtime environment, but the logic stays the same: datasource settings live in a profile file because they belong only to the database-backed mode.
It is also worth stating one more important thing that beginners often expect “intuitively”, but Spring Boot works differently: a profile file does not activate itself. The presence of application-postgres.yml in the project means nothing until you enable the postgres profile. In other words, the file is like a spare key: useful, but it will not open the door by itself.
4. Loading application files
When the application is packaged into a jar, the packaged config inside src/main/resources is only the baseline. Spring Boot can assemble the final configuration not only from what is already inside the artifact, but also from external files next to the launch point. That is why the same jar and the same image can start differently without rebuilding.
For now it is enough to keep three simple things in mind:
- application.yml and application-{profile}.yml inside the jar provide the packaged baseline;
- files with these same familiar names in normal external locations can override packaged values;
- on top of files, there are still targeted runtime overrides via env vars, -D, and --....
Here it is important not to mix two different scenarios. If a file has a familiar name and is located in an expected place, Boot often finds it by itself. But if the file name or the path become non-standard, that is no longer “just put it next to the app” — such a layer is connected explicitly.
So the scheme does not look like “either files or env vars”. Files provide a readable structure and repeating modes, while runtime overrides fine-tune a specific launch. And as soon as there are several active profiles, the order in which they are applied starts to affect the result too.
YAML instead of .properties
Spring Boot historically supports both application.properties and application.yml. You can use both formats, and in large companies you will really encounter anything — sometimes even in the same repository, sometimes even in the same folder (and then people wonder why “it lives its own life”).
But for a learning project and for a beginner developer, mixing formats almost always produces one effect: it is unclear which file won. The especially nasty situation is when YAML and .properties live in the same location, and someone “out of habit” changes one thing while something else is actually used.
Within the course we choose one clear teaching path: YAML everywhere. It is easy to read, especially when the configuration is nested (spring.datasource.url turns into a neat structure). YAML has its own “quirks” — for example, it loves indentation and hates it when you confuse spaces with tabs (and honestly, tabs are better avoided altogether; YAML takes them as a personal insult). But for us that is actually a plus: a single style across the whole project reduces the number of random errors.
So that you do not think this is “YAML religion” rather than a practical decision, the rule can be stated like this: one project — one format — one style. Then, when you look at src/main/resources/, your brain does not make unnecessary context jumps, and you understand faster where everything is.
5. Configuration layout in the project
Now it is useful to “ground” everything said above in the structure of our repository. We have already fixed the schema of configuration files in src/main/resources/:
src/main/resources/
├── application.yml
├── application-standalone.yml
├── application-postgres.yml
├── application-cache.yml
└── application-messaging.yml
Even if you still do not remember what exactly will be inside cache and messaging, the structure itself already says something important: the project can live in different modes, but it remains the same application.
For this lecture, the first three files are enough, because they show the main principle.
- application.yml keeps shared keys such as server.port and app.export-dir.
- application-standalone.yml adds the in-memory mode difference: app.storage-mode: memory.
- application-postgres.yml adds spring.datasource.* for the database-backed mode.
application-cache.yml and application-messaging.yml here matter only as markers for two more infrastructure contours. The full set of keys for them is not needed right now; what matters more is discipline: do not duplicate shared values in profile files.
If you catch yourself thinking “let us also duplicate export-dir in application-postgres.yml, because export exists there too…”, stop and ask yourself: how is export in postgres mode fundamentally different from export in standalone mode? Usually, it is not. That means export-dir stays shared. This is the main discipline of today’s lecture: do not smear shared things across profile files.
And one more small but useful thought: when a profile file becomes “half a screen long”, that is often a signal that you have started putting into it not “profile differences”, but “everything I found on the internet about Spring Boot”. This almost always ends with profiles becoming not modes, but alternate realities. In our course that is not allowed: we have one service, one architecture, one image — and different configuration modes.
6. Typical mistakes when splitting configuration
Mistake #1: making application-postgres.yml a full copy of application.yml, with a couple of lines added.
This usually happens “with the best intentions”: the developer is afraid that without a full copy the application “will not understand how to start”. But Spring Boot is specifically designed so that a profile file is a layer on top of the base file. Copying turns the config into a two-headed dragon: shared values start living in two places, and in a couple of weeks it is already unclear which one is current.
Mistake #2: storing mode-specific settings directly in application.yml mixed with shared ones.
This is very similar to a kitchen where sugar, salt, and pepper are all in one jar “because they are all white”. As long as you remember where everything is, you can live with it. Then a colleague shows up, and the quest begins. It is the same with configuration: if datasource parameters that are only needed in postgres live in the shared file, you lose the purpose of profiles and make mode maintenance harder.
Mistake #3: expecting application-standalone.yml or application-postgres.yml to “work by themselves”, without profile activation.
A file named application-postgres.yml is not a magic amulet that automatically enables postgres. It will be taken into account only when the profile is actually activated. If the profile is not activated, the application will honestly ignore this file, and that is the correct behaviour. Profiles are a switch, and you need to turn it on explicitly.
Mistake #4: repeating the same key in several profile files without a clear reason.
Sometimes it looks like a “safety net”: let there be the same value in application-postgres.yml and in application-cache.yml — “just in case”. In practice this becomes a mine: you enable two profiles, get last-wins, and suddenly behaviour changes because of the profile order. Each key should have one “main place”, and overrides should be intentional and rare.
Mistake #5: mixing YAML and .properties in the same location and then wondering why the application ignores your changes.
This is one of the nastiest mistakes, because a person “definitely changed the config”, but the application “definitely did not change”. Usually the reason is simple: you changed YAML, but the real value is overridden by .properties (or vice versa). In a learning project we deliberately choose one format so that such surprises do not become normal and do not eat up the time that should go to Docker and runtime behaviour of the service.
GO TO FULL VERSION