CodeGym /Courses /Docker for Spring /Clean configuration scheme

Clean configuration scheme

Docker for Spring
Level 12, Lesson 4
Available

1. Clean configuration scheme: the idea

To be honest, chaos in configuration usually appears not because people are “bad”, but because a project grows. At first, we have one application.yml, then PostgreSQL appears, then Redis, then RabbitMQ — and suddenly everyone adds their own parameters “so it works faster”. A week later this turns into a museum: here is application-final.yml, next to it application-docker.yml, and in README there are four startup commands that contradict one another. At that point the project can already be containerized, but maintaining it becomes unpleasant.

A clean configuration scheme is when you can answer questions like “what happens if I enable the cache profile?” and “why is the application looking for the database at this address?” without detective-level investigation. We want a simple result: each file has one responsibility, and the set of profiles combines into a mode like a Lego set, not like “a box of wires you should not touch”.

What matters here is that “clean” is not about beauty for beauty’s sake. In the Docker world, any extra configuration chaos automatically turns into two unpleasant things. First, you start rebuilding the image for different cases, violating the principle of same image, different runtime config. Second, you stop understanding what exactly was started in a particular container: why it writes files here, why it listens on that port, why it connects to Redis... and then the classic starts: “let’s add one more environment variable, maybe it will help”.

In this lecture, we will fix a canonical set of files and profiles so that it is lecture-friendly, but still very close to what real teams do: no hundred profiles, no copies of the same YAML, and no “one profile for every sneeze”.

2. Mode matrix: profiles and layers

To keep the configuration from spreading, first we need to agree honestly on which axes of variation the application has. In our training service, it is not “all possible Spring Boot options”, but only a few infrastructure contours, because the project is intentionally simple. And that is good news: the fewer axes you have, the lower the chance of getting application-please-help.yml.

Keep this picture in mind: there is a base mode in which the service can live by itself (this is standalone), and there is a mode where an external database appears (postgres). On top of postgres, we can “tune in” two additional dependencies — Redis (cache) and RabbitMQ (messaging). That is, cache and messaging are not independent “worlds”, but additional layers.

For clarity, it is useful to see this once as a table. This is not a list of commands and not “how to launch it”, just a map of differences:

Profile(s) Catalog storage PostgreSQL Redis RabbitMQ Mode idea
standalone in-memory no no no fast startup without infrastructure
postgres JPA + DB yes no no normal persistence model
postgres,cache JPA + DB yes yes no added cache for reads
postgres,cache,messaging JPA + DB yes yes yes added minimal messaging wiring

The key methodological point is that we do not try to support every combination. For example, we do not consider standalone,cache a normal mode. Theoretically it is possible, but it raises questions like “cache of what, if there is no database?”, “what about seed data?”, “what are we caching?”. For a Docker course, this combination creates more confusion than value.

And one more important point: a mode is a composition of profiles, not “the name of one profile that solves everything”. That is why we deliberately choose short profile names by meaning (and not docker, prod2, mode_test). A profile should answer the question “what changes”. If the profile is called postgres, you will not have to guess what it does.

3. The “one key — one home” rule

Now comes a rule that looks boring, but saves you from configuration apocalypse: each property should have one primary home. It may be overridden from outside (that is fine), but it should not be “declared as truth” simultaneously in three profile files. Otherwise you get the “last-wins” effect where you really did not want a guessing game.

Imagine that configuration is a wardrobe. application.yml is everyday clothes: jeans, a T-shirt, a jacket. Profiles are “special gear”: a helmet for construction (postgres), gloves for working with metal (cache), a radio for communication (messaging). The problem starts when you begin storing jeans in both the helmet and the gloves. At that point it is no longer a wardrobe, but a quest.

What do we put in the base application.yml? Everything that is the same for all modes and reflects the service “personality”: the port (or its default), basic Actuator settings, our app.export-dir (meaning some default path), and any general logical flags. But anything that depends on a specific infrastructure (database URL, Redis host, RabbitMQ host) should live in profile files, because by meaning it is not “always”, but “only if this contour is enabled”.

Let us also fix an important nuance: profile files should not be a copy of application.yml. Their job is to be short and “targeted”. The shorter application-postgres.yml is, the faster you understand what exactly the postgres mode enables.

To make this concrete, let us define the distribution of keys across files once. In table form, it is easier to read than a long moral lecture:

Settings group Where the “home” is Example keys
General service settings application.yml server.port, management.*, app.export-dir
Standalone storage application-standalone.yml app.storage-mode: memory (or a similar marker)
PostgreSQL datasource application-postgres.yml spring.datasource.*, spring.jpa.*, spring.flyway.* (if needed)
Redis wiring application-cache.yml spring.data.redis.*, plus your narrow flag such as app.cache-enabled
RabbitMQ wiring application-messaging.yml spring.rabbitmq.*, plus app.messaging-enabled

If you notice that the same property wants to be written into two profile files, that is usually a signal: either the property is actually general (which means it belongs in application.yml), or you are trying to “reassign” someone else’s responsibility through a profile (for example, the cache file tries to manage the datasource).

4. Resource structure: five files

Up to this point, we have been looking at properties piece by piece. Below we combine them into one working file set; this is what you should keep in mind as the project baseline, not separate fragments of properties.

We have already said many times “keep files short”, but in a real project this starts with simple discipline: exactly five clear files in src/main/resources, with no author poetry in the names. When a student (or colleague) opens resources, they should immediately see a map of modes, not a riddle in three letters.

In our end-to-end project, it looks like this:

src/main/resources/
├── application.yml
├── application-standalone.yml
├── application-postgres.yml
├── application-cache.yml
└── application-messaging.yml

Why is this especially important for a Docker course? Because Docker likes reproducibility, and reproducibility likes predictability. If the config is laid out cleanly, you can run the same image with SPRING_PROFILES_ACTIVE=postgres,cache and be sure: Redis will be enabled, while the datasource will remain postgres-based, and nothing will “accidentally” be overwritten.

Another small but useful discipline: within the course, we do not mix YAML and .properties in one location. Technically Spring Boot supports both, but for a beginner this creates an unnecessary surprise: two files in one folder with similar meaning, and suddenly the priority is not the one you expected. We want “last-wins” to exist only where we consciously allow it (the order of profiles), not because someone randomly added application.properties.

And finally: we do not put spring.profiles.active inside application-postgres.yml and similar files. A profile is something that comes “from the outside” at startup. A profile file should describe differences, not try to enable itself (it sounds energetic, but in the end you will either get confused or get implicit behavior).

5. Config examples for Catalog Service

Now we move to full packaged baseline variants, not separate property fragments. We will make them friendly to external overrides via env vars, which you already know how to pass into a container.

application.yml — only general settings

application.yml should be boring in a good way. If it looks like an “encyclopedia of all Spring settings”, you have already lost.

# src/main/resources/application.yml

spring:
  profiles:
    default: standalone # Default profile if no active profiles are specified at startup

server:
  port: ${SERVER_PORT:8080} # The port can be overridden via an environment variable

app:
  export-dir: ${APP_EXPORT_DIR:/app/exports} # Base path for exports, also overridable from outside

Here we can see three things: there is a clear default profile, the port can be overridden from outside, and the export directory is also overridable. We are not talking about where /app/exports physically lives — that is file-system mechanics, and today we are staying focused on the configuration scheme.

If you want to add Actuator exposure, you can, but keep it short and only if it is truly general for all modes. This is still the same application.yml:

# src/main/resources/application.yml

management:
  endpoints:
    web:
      exposure:
        # The endpoint list can be overridden from outside (useful for different environments)
        include: ${MANAGEMENT_ENDPOINTS_WEB_EXPOSURE_INCLUDE:health,info,metrics}

application-standalone.yml — differences for in-memory mode

Standalone should start quickly and should not require external services. Therefore, in the profile file we describe only what makes this mode different.

# src/main/resources/application-standalone.yml

app:
  storage-mode: memory # In standalone, we use in-memory storage

Yes, the file turned out “funnily small”. And that is the perfect compliment. If a mode differs by one key, it means you designed the scheme well.

application-postgres.yml — datasource and everything related to the DB

In postgres mode, a database appears, and therefore a datasource. Here it is logical to keep the datasource “home” in this file.

# src/main/resources/application-postgres.yml

app:
  storage-mode: postgres # Switch storage mode to the database

spring:
  datasource:
    # All key parameters can be overridden via env vars (important for containers/Compose)
    url: ${SPRING_DATASOURCE_URL:jdbc:postgresql://localhost:5432/catalog}
    username: ${SPRING_DATASOURCE_USERNAME:catalog}
    password: ${SPRING_DATASOURCE_PASSWORD:catalog}

Notice an important psychological effect: even if you are not running in Compose yet, but just in a container “in a vacuum”, the datasource home stays here. Do not move spring.datasource.* into application.yml “because it is faster that way”. That very “faster” later turns into “why is standalone trying to connect to the database?”.

application-cache.yml — Redis as an additional layer

The cache profile should be an add-on, not a “second version of postgres”. We do not duplicate the datasource and we do not change the server port. We simply add Redis wiring and, if desired, one small flag for our own beans.

# src/main/resources/application-cache.yml

app:
  cache-enabled: true # Enable the cache contour (useful for conditional bean registration)

spring:
  data:
    redis:
      # Redis parameters are expected from outside (in containers, the host is almost never localhost)
      host: ${SPRING_DATA_REDIS_HOST:localhost}
      port: ${SPRING_DATA_REDIS_PORT:6379}

Even if you have not implemented a complex cache scenario yet, this file is still useful: it fixes in advance that Redis is a separate contour enabled by a separate profile.

application-messaging.yml — RabbitMQ as an additional layer

The same applies to messaging: an add-on, not a “new world”.

# src/main/resources/application-messaging.yml

app:
  messaging-enabled: true # Enable the messaging contour (conditional bean/listener registration)

spring:
  rabbitmq:
    # RabbitMQ parameters that are convenient to pass via env vars/Compose
    host: ${SPRING_RABBITMQ_HOST:localhost}
    port: ${SPRING_RABBITMQ_PORT:5672}

Notice that all parameters are tuned for external overrides. That is exactly the scheme that makes containerization honest: inside the jar there are reasonable defaults, and from the outside you can override them without rewriting the artifact.

6. Useful nuances

Profiles in mode: default, active, and last-wins

In this scheme spring.profiles.default: standalone lives in application.yml and gives a fallback startup without external infrastructure. If startup says nothing explicit about profiles, the service will come up in in-memory mode and will not suddenly start looking for PostgreSQL, Redis, or RabbitMQ.

The concrete mode is still set from outside through spring.profiles.active: postgres, postgres,cache, or postgres,cache,messaging. That means the packaged baseline stays the same, while the required combination of contours is chosen at container startup or with java -jar.

If multiple profiles are active, files overlay in their order, and the later profile can override the earlier one. That is exactly why we distributed keys by role: postgres is responsible for the datasource, cache for Redis, messaging for RabbitMQ. Then last-wins remains a rare conscious case, not an everyday roulette.

External overrides and packaged baseline

With this five-file scheme, the service should already start normally even without external files: the packaged baseline inside the jar is self-sufficient. External config here is not a “second truth”, but a thin override layer needed only when a particular machine or a particular run needs a different value.

If ordinary application.yml / application-{profile}.yml files are placed externally in the standard location, Spring Boot can pick them up automatically. If you choose an arbitrary name like runtime.yml or a non-standard path, such a file is attached explicitly via spring.config.import, spring.config.additional-location, or spring.config.location. What matters is not mechanics for its own sake, but the rule: the runtime layer changes, not the jar and not the image.

Checking the active mode without endpoints

After building the scheme, it is useful to check not “it seems to work”, but two concrete signals at startup: which profiles are active and what final value one or two characteristic keys such as app.export-dir got. Such a smoke check quickly shows whether the packaged defaults, external override, and env vars came together in the order you expected.

You can print this to the startup log in any simple way: through ApplicationRunner, a regular logger, or even a temporary System.out.println in a training project. The point is not the beauty of the implementation, but to avoid guessing whether you started in standalone or already in postgres,cache, and whether you are accidentally carrying over an old app.export-dir value.

7. Typical mistakes in the final configuration scheme

In this topic, mistakes almost always look the same: the application “should work like this”, but starts “kind of differently”. And almost always the reason is not Docker or Spring Boot as “black magic”, but the fact that the configuration has lost its structure. Below are the most common pitfalls worth recognizing on sight.

Mistake #1: application-{profile}.yml turns into a copy of application.yml.
This looks “safer”: as if each profile were self-sufficient. In practice you get five almost identical files, where differences are hard to notice and changes must be made in five places at once. In the end someone forgets to change one place, and the “why is the port different only in postgres mode?” story begins.

Mistake #2: the same key lives in several files “just because it happened that way”.
For example, spring.datasource.url ended up both in application.yml and in application-postgres.yml. This is dangerous because you stop understanding the property “home”. At some point you change one value, but the application keeps taking another one, and you start suspecting mysticism. In reality it is just a conflict of sources.

Mistake #3: the cache or messaging profile starts duplicating postgres settings.
Additional profiles should add differences, not be a “second version of postgres mode”. If application-cache.yml suddenly starts containing a datasource, you have already lost the idea of composition: enabling cache should affect only the cache contour, not rewrite the base data-access mode.

Mistake #4: trying to “enable a profile from inside a profile”.
Sometimes you want to write spring.profiles.active: postgres inside application-postgres.yml. It looks logical, but in practice it breaks predictability: the profile should come from outside at startup, otherwise you get strange cascades and cannot understand by looking why what got activated got activated.

Mistake #5: the external config becomes mandatory, and without it the service does not start.
When the external file is not marked as optional: (or you use spring.config.location in a way that disables the standard lookup locations), you easily end up with “it works on my machine, not on yours”. For the course and for the template, it is important that the base packaged path stays working: download the project, run it, it lives.

1
Task
Docker for Spring, level 12, lesson 4
Locked
Machine-dependent override on top of the clean configuration layout
Machine-dependent override on top of the clean configuration layout
1
Survey/quiz
Spring Config, level 12, lesson 4
Unavailable
Spring Config
Profiles and configuration settings
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION