CodeGym /Courses /Spring REST & MVC /Final configuration discipline

Final configuration discipline

Spring REST & MVC
Level 28, Lesson 4
Available

1. Configuration discipline

By this point, we already have all the pieces: runtime parameters have been moved out of the code, the basic application.yml has become readable, app.attachments.* reaches storage through AttachmentProperties, and the local override lives on top of the base config. Now all that is left is to assemble this into one reproducible project baseline and quickly check three things: whether there is any hardcoding left, whether the multipart limits conflict with each other, and whether a second source of truth for storage-dir has appeared. So, let's get started.

When a project is small, it is very easy to believe the myth: “the main thing is that it starts, and we will clean up the config later.” That is roughly the same as saying: “the main thing is that the light bulb turns on, and we will neatly route the wiring later.” Technically, the light will be on, but one day you will accidentally catch the wire with your foot — and suddenly discover what “why did it work yesterday?” means.

Right now, the important thing is not to argue again about what should be moved into YAML at all. We have already closed that question. Here the task is more practical: make sure the Task Tracker API starts the same way for the whole team, and the config does not hide a second copy of the logic. Framework settings live under spring.*, application settings live under app.*, runtime parameters are changed through the base config, profile, and overrides, and the API contract and domain rules remain in the code.

2. Required startup keys for Task Tracker API

If we look at configuration as a small contract between “code” and “environment”, then this contract should have a minimal required set of items. Otherwise, we end up with a situation where the application starts “somehow”, errors look random, and you are debugging not the task, but your own environment. In an educational project, we do not need universality; we need predictability, so it is better to have a few required keys, but keep them in one clear place.

In our project, there is a good natural split: everything related to Spring MVC and servlet infrastructure lives under spring.*, and everything related to the startup rules of our application lives under app.*. This is not a “rule for the sake of a rule”, but a way to make the file readable: even a person who opened application.yml for the first time will quickly understand where the “platform settings” are and where the “project settings” are.

Below is a table of the minimum required keys that should be kept in the base configuration, not in a profile, so that Task Tracker API starts the same way for everyone.

Key Example value Why it is needed Level
spring.mvc.problemdetails.enabled true Enables Problem Details as the base error format (application/problem+json) framework-level (spring.*)
spring.servlet.multipart.max-file-size 10MB Limits the size of a single file during upload framework-level (spring.*)
spring.servlet.multipart.max-request-size 12MB Limits the total size of the multipart request (file + metadata + service parts) framework-level (spring.*)
app.attachments.storage-dir ./data/attachments Tells the storage layer where to keep file contents application-level (app.*)

This set is enough for the project's runtime baseline to be explicit rather than scattered across the developers' memories.

3. Aligned multipart limits

Multipart limits are one of those cases where a config can be “syntactically correct” but “semantically wrong”. And that is especially unpleasant because the error appears somewhere other than where you expect it: the request will not even reach your controller, and you will look at the log and think, “why is my endpoint not being called, I definitely wrote it”.

In multipart, we have at least two quantities: the maximum size of one file and the maximum size of the entire request. To simplify with a real-world analogy, max-file-size is the “size of one suitcase” limit, and max-request-size is the “total baggage weight” limit. Logically, the total baggage should be not smaller than one suitcase, otherwise you allowed a 20 kg suitcase but set the trunk limit to 10 kg. Formally, you did nothing wrong, but the trip will end at the first checkpoint.

An example of a bad configuration that looks “fine enough” but in practice creates unpredictability:

spring:
  servlet:
    multipart:
      # Contradiction: one file can be 20MB...
      max-file-size: 20MB
      # ...but the entire request is only 10MB
      max-request-size: 10MB

Now you are contradicting yourself: one file may be 20MB, but the entire request is only 10MB. As a result, for the user this will look like “maybe 413 Payload Too Large, or some weird upload error”, and for you it will look like “why does it not work if I allowed 20MB”.

A good baseline strategy for our project is simple: since we upload one file and a small JSON metadata block, max-request-size should be slightly larger than max-file-size. For example, 10MB and 12MB make a clear pair. Plus, it reads well at a glance: you can see that the request includes more than just the file.

spring:
  servlet:
    multipart:
      # One file
      max-file-size: 10MB
      # The entire request (file + metadata)
      max-request-size: 12MB

Another nuance: it is very easy to set huge values “just in case” so that “it definitely won't get in the way.” But in an educational project, that is counterproductive: it is more important to learn how to control boundaries than to make “infinite upload”. Large limits increase the risk that you will start keeping in memory or logs things you should not keep there, and generally make debugging harder.

4. Explicit defaults

Defaults in configuration are where beginners most often fall into a trap: either they do not set a default at all (“let it be whatever”), or they set a default in such a way that the project becomes tied to one machine (“everything looks nice on my D drive”). The truth here is simple: the base value should be portable and visible directly in the base config.

For app.attachments.storage-dir, that value is ./data/attachments. This is not a “secret fallback in a Java class”, but an explicit project default. You can see it immediately after opening application.yml, and therefore any developer understands where files will go by default.

app:
  attachments:
    # Base path for the repository; local differences are overridden by a profile or env var
    storage-dir: ./data/attachments

That is exactly why AttachmentProperties should not invent a second fallback inside the code. If storage-dir is a critical property, the base config sets it explicitly, and @Validated and @NotBlank protect you from an empty value at startup. Otherwise, you end up with two truths: YAML says one thing, and the Java class quietly substitutes another.

5. Removing hardcoding from storage

A configuration is considered done not when you add a beautiful YAML file, but when you remove the last “magic strings” from the code that contradict the idea of external settings. A very typical situation: you added app.attachments.storage-dir, but somewhere in storage there is still Path.of("/tmp/...") because “I was just testing.” A week later, you no longer remember that it was a test, and you start wondering “why are the files not there?”

After AttachmentProperties, storage has one honest job: take the typed config object and work with it. It should not choose the path on its own, keep a backup constant, or argue with YAML.

package com.example.tasktracker.infrastructure.storage;

import com.example.tasktracker.config.AttachmentProperties;
import java.nio.file.Path;
import org.springframework.stereotype.Service;

@Service
public class LocalAttachmentStorage {
    private final Path root;

    public LocalAttachmentStorage(AttachmentProperties props) {
        // The only source of truth about the directory is the typed config object
        this.root = Path.of(props.storageDir());
    }
}

Yes, you can add path normalization (normalize()), conversion to absolute (toAbsolutePath()), directory creation (Files.createDirectories(root)). But those are already details of the file storage implementation. In the context of today's lecture, the important thing is to lock in the discipline: storage does not choose the path itself and does not keep it in the source files.

6. Configuration validation

Configuration validation is a “safety net against human error”. And it is especially useful where, without a setting, the subsystem physically cannot work. For storage-dir, this is exactly our case: an empty or missing path should break startup immediately, not surface during the first upload as Path.of(null) or a strange 500.

This part is already handled for us by @Validated and @NotBlank in AttachmentProperties. For the final baseline, one idea is important: storage-dir remains a required application-level key, the base application.yml gives it an explicit value, and fail-fast on startup does not allow the application to enter a half-working state.

Another useful boundary: do not mix up “config validation” and “request validation” in your head. When a user sends bad JSON, that is 400 Bad Request and our error contract. When the application has a broken config, that is a startup problem, and the correct behavior is usually one thing: the application must not start.

7. Baseline application.yml

At the final stage, you want to do two things at once: keep the file short and make it explicit enough so that you do not depend on “Spring Boot defaults that I do not remember”. That is not a contradiction if you keep the discipline: you lock down exactly the settings that affect the behavior of your application scenario, rather than trying to “configure all of Spring”.

Below is an example of a base application.yml that fits our current project state well. It sets Problem Details, multipart limits, and storage-dir. Everything else is left to defaults, because either it does not affect the scenario or it requires separate discipline, which we intentionally are not expanding in this course.

spring:
  mvc:
    problemdetails:
      # A single error format for the API
      enabled: true
  servlet:
    multipart:
      # One uploaded file
      max-file-size: 10MB
      # Entire multipart request (file + metadata)
      max-request-size: 12MB

app:
  attachments:
    # Base storage directory; profile and env vars may override it
    storage-dir: ./data/attachments

In this fragment, what matters is not only the specific values, but also the fact that you can now “read it like a document”. Right in the YAML, you can see what the project expects: errors in Problem Details, file uploads up to 10MB, and storing attachments in ./data/attachments. That is the goal of discipline: to make configuration stop being the “secret knowledge of the project author” and become an explicit part of the repository.

8. Common configuration mistakes

Mistake No. 1: leaving part of the hardcoding “just in case”.
Very often, after introducing @ConfigurationProperties, an old constant remains in the code, for example Path.of("/tmp/task-tracker/uploads"), or a silent fallback inside AttachmentProperties, and it starts arguing with YAML. As a result, the config exists, the typed object exists, but the files still go to the old place. Once the path has been moved into configuration, there should be no second hidden truth about the directory left in the storage implementation or the config class.

Mistake No. 2: make max-request-size smaller than max-file-size.
Such a configuration is formally correct, but logically contradictory. It causes uploads to start failing “before the controller”, and you end up fixing the wrong place. A good rule: if multipart carries one file and a small metadata part, then max-request-size should be a little larger than max-file-size, so you do not get the feeling that “it uploaded today, and tomorrow it will not”.

Mistake No. 3: move into YAML something that is part of the API contract.
Moving DTO constraints, status rules, allowed transitions, and restrictions on title into config is an architectural mistake. Those things should live in code, because they describe the API contract and the domain, not the startup environment. Otherwise, you get a situation where one developer “tweaked the YAML”, and clients suddenly saw different behavior without an API version change.

Mistake No. 4: fail to distinguish spring.* from app.* and create a “flat pile of keys”.
When properties are mixed at the root of the file without structure, YAML stops being a document. A person reads a “bag of keys” and does not understand what controls what. The discipline here is simple: everything related to the platform (MVC, multipart) stays in spring.*, and everything related to your application domain (attachments.storage-dir) lives in app.*.

Mistake No. 5: do not validate critical application-level properties.
If storage-dir is required, then lacking validation means the application can start and then fail when a file is uploaded. That is the worst case: the problem is discovered late, in an unpredictable place, and usually with a less understandable error. @Validated and @NotBlank on AttachmentProperties are a simple way to make the behavior “either we start correctly, or we do not start at all”.

1
Task
Spring REST & MVC, level 28, lesson 4
Locked
Minimal configuration baseline for the upload scenario
Minimal configuration baseline for the upload scenario
1
Task
Spring REST & MVC, level 28, lesson 4
Locked
Upload endpoint with a configurable storage directory
Upload endpoint with a configurable storage directory
1
Survey/quiz
Spring Settings, level 28, lesson 4
Unavailable
Spring Settings
Settings, profiles, and validation
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION