1. The configuration is set, but the behavior is different
If you have ever changed application.yaml, restarted the service, and still seen the old behavior — congratulations, you have officially joined the club of “I definitely changed that, honestly!”. In the world of Spring Boot, this is not shameful: we have many sources of properties (YAML, profiles, environment variables, startup parameters), and they have priorities. As a result, the application may work correctly... just not the way you expected.
The problem is not even that there is a lot of configuration. The problem is that without tools we start guessing: “Probably the profile is wrong,” “Probably the environment variable overrode the YAML,” “Probably the bean was not picked up,” “Probably I did not restart the application at all (oops).” And that is where Actuator becomes your “lie detector” — in the best sense: it shows facts, not our fantasies.
Imagine a typical situation in catalog-service. We have a property app.catalog.max-featured-count. It affects the GET endpoint /api/catalog/featured: how many courses will be included in the selection. You change the YAML, but the response still shows 4 courses. Possible reasons are boring, but real: an env var override, the wrong profile was picked up, the default value in the record overrode expectations, or you launched the application from the wrong place entirely.
Actuator offers two different “X-rays” of configuration. The env endpoint answers the question “which value won and where did it come from?”. The configprops endpoint answers the question “which values were actually bound into the @ConfigurationProperties model, and with which origins?”. Together they turn diagnostics from guessing into an engineering procedure.
2. Environment and @ConfigurationProperties
Before you start calling endpoints, it is important to get the mental model straight: in Spring Boot there are two layers of configuration work that are very close in meaning, but still different. The first layer is Environment: it contains everything Boot collected from property sources, applied precedence to, and prepared as the “final picture of the world”. The second layer is binding into @ConfigurationProperties: when values from Environment become your type-safe CatalogProperties with int, boolean, List<...>, and so on.
A good everyday analogy: Environment is like a giant “key → string value” table that Boot collected from different places, sorted by importance, and put on the table. And @ConfigurationProperties is like the process of “building a proper object from this table”, where max-featured-count: "4" becomes int 4, and a list of courses from YAML becomes List<CourseConfig>. And these two layers are checked by different tools.
Let us draw this as a data flow (simplified, but truthful in meaning):
flowchart TD
A["Property Sources
YAML / profiles / env vars / CLI args"] --> B["Environment
final values + precedence"]
B --> C["@ConfigurationProperties binder
type-safe binding"]
C --> D["CatalogProperties (record)"]
D --> E["Your beans (Service/Controller)
use properties"]
B --> F["Actuator /env
looks at Environment"]
C --> G["Actuator /configprops
looks at bound properties beans"]
Notice that env and configprops really answer different questions. And this is not “two identical endpoints because the Actuator authors were bored.” These are two different angles.
To make it even easier to remember, here is a small table of “what you see where”:
| Question | Where to look | Typical wording of the problem |
|---|---|---|
| Which value won? | /actuator/env | “Why does the property have the wrong value?” |
| What did the value bind into? | /actuator/configprops | “Why is CatalogProperties.maxFeaturedCount() not what I expected?” |
And now an important nuance: different representations of the same property in different places may look different. YAML prefers kebab-case, Java prefers camelCase, and environment variables prefer UPPER_SNAKE_CASE. Boot knows how to “stitch” this together (relaxed binding), but for a beginner’s brain it can sometimes look like magic.
Here is a small “translator” that is useful to keep nearby:
| Where you see the setting | How it is written | Example for maxFeaturedCount |
|---|---|---|
| application.yaml | kebab-case | max-featured-count: 4 |
| Property key (in Boot terms) | dotted | app.catalog.max-featured-count |
| Java record component | camelCase | maxFeaturedCount() |
| Env var | UPPER_SNAKE_CASE | APP_CATALOG_MAX_FEATURED_COUNT=4 |
With this foundation, you can calmly move on to env and configprops and not be alarmed that the same value “puts on different costumes”.
3. /actuator/env: value and source
The env endpoint is your way to look at configuration through the eyes of Environment. Its power is not that it “shows all settings” (although it can), but that it can show: which value is set right now, and, most importantly, where it came from. This directly treats the most common diagnosis in the world of configuration: “well, it is definitely in my YAML...” (when in fact it is an env var).
To work with env, the endpoint needs to be enabled over HTTP in the current profile. In our course we usually do this only for the local (and sometimes dev) profile. An example for application-local.yaml looks minimally like this:
management:
endpoints:
web:
exposure:
include: "health,info,env,configprops" # Enable diagnostic endpoints only for local/dev
To see the actual values, and not just keys and sources, sometimes you need to temporarily switch on a more open local debug mode. By default it is safer to keep show-values: never, and enable raw values one time only while troubleshooting — for example, via --management.endpoint.env.show-values=always and --management.endpoint.configprops.show-values=always.
After that, you can use the browser or curl. The most important practical trick: do not start with the full /actuator/env, otherwise you will drown in JSON like in an ocean. Start with a targeted request for the property you care about.
For example, we want to understand what value the application sees for app.catalog.title. The request looks like this:
curl http://localhost:8080/actuator/env/app.catalog.title
The env response is fairly “verbose” because it tries to be helpful. But usually we care about two things: the value and the source. Simplified (and with a few details trimmed) it looks roughly like this:
{
"property": {
"value": "Spring+ Catalog",
"source": "systemEnvironment"
}
}
This means: “the value from environment variables won.” And right away it becomes clear why your YAML did not work: it was simply overridden from above. Without env you could spend half an hour changing YAML, suspect Gradle caches, blame the IDE, remember ancient curses... and the problem would have been a single variable APP_CATALOG_TITLE.
To feel the power of this tool, let us imagine a short (and very real) scenario: in application.yaml you have one value, and you want to temporarily override it at startup without editing the file. You start it like this:
# Temporarily override the value without editing the YAML
APP_CATALOG_TITLE="Catalog (override from env)" \
java -jar build/libs/catalog-service.jar
And then you check:
curl http://localhost:8080/actuator/env/app.catalog.title
If everything worked, env will show that the value came from systemEnvironment. If it did not, env will honestly show what actually won. For a developer, this is almost therapeutic: fewer emotions, more facts.
Another common case: “I activated the local profile, but it seems like the application started in the default one.” This can also be checked through env, because it usually shows active profiles and the set of property sources. The full /actuator/env response contains blocks such as activeProfiles, defaultProfiles, and propertySources. The full JSON is huge, but you do not need to memorize it. The important idea is this: propertySources are listed in order, and sources higher in the list usually have higher priority.
And one last very practical note: env is useful not only for our app.catalog.*. It also helps explain why the port suddenly did not change (server.port), why a profile did not activate (spring.profiles.active), or why some flag was turned on/off. It is a universal endpoint that says: “show me what you really see in the environment.”
4. /actuator/configprops: the final binding
If env is about “what values exist in the environment at all,” then configprops is about “what values actually bound into your typed properties”. This becomes especially important after you move to @ConfigurationProperties, because now your application relies not on strings, but on the CatalogProperties object. And sometimes the problem is not that the value is wrong, but that the binding did not happen the way you expected.
Let me remind you of a typical fragment from our catalog-service. In its simplest form, the model may look like this:
package com.example.catalogservice.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties(prefix = "app.catalog")
public record CatalogProperties(
String title, // Catalog title that the client will see
int maxFeaturedCount, // How many courses will go into the featured selection
boolean startupReportEnabled // Whether to enable the diagnostic report at startup
) {
}
Here we already have types that need to be bound: a string, a number, a boolean. And in a real project there will also be a list of courses and nested structures — and that is where configprops starts to shine the brightest.
The request to the endpoint looks like this:
curl http://localhost:8080/actuator/configprops
The same logic applies to configprops: if you keep the safe mode by default, the structure of the endpoint will remain the same, but the values will be hidden. So the JSON below also refers to the temporary local debug mode where raw values are allowed.
But, as with env, the full response is huge. The normal strategy is to search this JSON for a specific prefix (app.catalog) or a specific bean (catalogProperties). In the configprops response there is usually a contexts section, and inside it a set of beans, where each bean is described: what its prefix is, what values were bound into it, and where they came from (origin).
A simplified (and very trimmed) example of what you would see for CatalogProperties might look like this:
{
"beans": {
"catalogProperties": {
"prefix": "app.catalog",
"properties": {
"title": "Spring+ Catalog",
"maxFeaturedCount": 4,
"startupReportEnabled": true
}
}
}
}
Why is this more useful than env, if the values are the same? Because configprops shows the result of the binding itself. For example, if you had max-featured-count: "four" in your YAML by mistake, env could show the string "four", but the application would fail to start because it could not bind an int. And if you have a more subtle situation with defaults, nested objects, and lists, configprops shows the final structure exactly as your code receives it.
Another important point: configprops helps you see what is actually registered as an @ConfigurationProperties bean. For that, of course, the registration condition must be satisfied. In the course project we use the normal modern approach — @ConfigurationPropertiesScan on the main class:
package com.example.catalogservice;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
@SpringBootApplication
@ConfigurationPropertiesScan // Enable scanning and registration of @ConfigurationProperties classes
public class CatalogServiceApplication {
public static void main(String[] args) {
SpringApplication.run(CatalogServiceApplication.class, args);
}
}
If scanning is not enabled (or CatalogProperties lives in a package that is not included in the scan), the bean may not appear, and configprops will not show it. Sometimes this is faster to detect through configprops than by reading a long stack trace or searching the project by eye.
In short, configprops is the endpoint that says: “show me my properties objects as they are bound right now.” And for a config-first application, it is almost like opening the hood of a car and seeing that the engine is really there, not just drawn in a presentation.
5. Scenario: featured selection
Now let us combine both endpoints into one coherent scenario that is easy to imagine in our catalog-service. Suppose we have the setting app.catalog.max-featured-count, which limits the GET endpoint /api/catalog/featured. And then you change the value, but the response still seems “old”. How do you act like an adult, without waving a magic wand over YAML.
First, we fix the expected connection “config → behavior”. The logic in the service (very roughly) may be this: the service takes properties.maxFeaturedCount() and trims the featured courses list to that size. It does not matter where exactly you did this — in CourseCatalogService or in the controller; what matters is that somewhere in the code CatalogProperties is used.
Suppose in application-local.yaml we set:
app:
catalog:
max-featured-count: 2 # We want /api/catalog/featured to return at most 2 courses
We start the service in the local profile and go to our domain endpoint:
curl http://localhost:8080/api/catalog/featured
And suddenly we see that not 2 courses are returned, but 4. This is where many people start “rechecking the code”. But it is better to start with configuration, because it is the cheapest and most common source of surprises.
We go to env, but not fully — in a targeted way:
curl http://localhost:8080/actuator/env/app.catalog.max-featured-count
If you see a mask instead of a number, that means safe mode is enabled as it should be; for such a targeted investigation, raw values are usually enabled only locally and only for a couple of minutes.
If env shows the value 4 and the source systemEnvironment, then somewhere there is APP_CATALOG_MAX_FEATURED_COUNT=4. And now you know what is happening: the YAML was not ignored, it just lost by priority. If env shows 2 and the source applicationConfig: [classpath:/application-local.yaml], then the value in Environment is correct, and we need to understand: did it bind the same way in the typed model?
Here we use configprops. Yes, the response is large, so in real life you either search for the string "app.catalog" or for the bean name. But the point is simple: you find CatalogProperties and look at what maxFeaturedCount equals.
If it is still 4, then you have two options: either it is the wrong bean (for example, there are two different properties classes with similar names in the context), or you have defaults in code/config that you misinterpreted. If configprops shows 2, then typed binding is correct, and the problem is no longer in the configuration itself, but in the fact that the domain endpoint does not use this value where you thought it did. But notice how important this is: you localized the problem. It is no longer “Spring Boot is broken”; it is a specific section of code or a specific expectation.
To show the idea of the connection even more clearly, you can mentally picture this “verification triangle”:
flowchart LR
A["/actuator/env
value and source"] --> B["/actuator/configprops
bound model"]
B --> C["/api/catalog/featured
observable behavior"]
C --> A
If the values on the env and configprops sides match, but the behavior does not — the issue is in the code (or in another config that affects behavior indirectly). If env does not match expectations — the issue is in precedence/source. If env matches, but configprops does not — the issue is in binding/registration of the properties bean. This is a very simple, but incredibly practical, way to debug.
And yes, this is one of those rare cases where “check through Actuator” is actually faster than “temporarily add a log in the code”. Logs are still useful, but env/configprops give information more precisely and without extra noise.
6. Boundaries: what we do NOT do with env and configprops
Actuator endpoints related to configuration are very much like an X-ray: useful in a doctor’s office, strange at a party, and definitely not something for a billboard by the road. By nature, env and configprops are diagnostic, not product endpoints. We do not build business logic on them, do not write client integrations “that will always call /actuator/env”, and certainly do not treat them as part of the catalog’s public API.
In the course, we keep them available only where they really help the developer: in the local (and sometimes dev) profile. In prod, the minimal set remains by default, because configuration and its sources can be sensitive. Even if the values themselves are “not secret,” the sources and structure of the configuration can reveal internal service details.
Another important boundary is not turning env and configprops into a “documentation replacement”. They are not for another developer to study your service by poking at JSON. They are for diagnostics: quickly understand “what is really happening” in a running application instance. Therefore, the normal practice is to open, look, draw a conclusion, close. And return to the habit of writing clear application.yaml files and sensible @ConfigurationProperties names, so diagnostics are rare rather than daily.
7. Common mistakes when working with env and configprops
Mistake #1: opening /actuator/env in full and trying to read it like a short report.
In practice, this is not a “convenient summary”, but a large JSON in which it is easy to lose the thread. In the end, diagnostics turn into random browsing of everything. It is much more useful to go targeted: /actuator/env/<property-name>, so you immediately see the specific property and the source it came from.
Mistake #2: confusing the roles of env and configprops.
These endpoints answer different questions. env is needed when you want to understand where a property came from and which value won by precedence. configprops is needed when you want to see the final bind process for an @ConfigurationProperties class, for example CatalogProperties. If you ask them the “wrong” question, it quickly creates the false impression that Actuator “shows nothing”.
Mistake #3: forgetting about the different forms of a property name.
In YAML, Java, and environment variables, the same property looks different: app.catalog.max-featured-count, maxFeaturedCount, APP_CATALOG_MAX_FEATURED_COUNT. One wrong character, dash, or underscore — and now you are convinced that “Boot did not pick up the setting”. In such cases, env is especially useful: it quickly shows whether the value made it into the Environment at all.
Mistake #4: expecting configprops to show all application properties.
It is not a universal list of every possible setting. configprops shows only those properties that were actually bound into @ConfigurationProperties beans. Therefore, looking there for any arbitrary application key is the wrong expectation. This endpoint is not for a full overview of the configuration world, but for checking typed binding.
Mistake #5: treating env and configprops as an external product API.
Sometimes there is a temptation to use Actuator endpoints as a “ready-made interface” for the front end or external clients. That is a bad idea: diagnostic endpoints should not become part of the user contract. They are for engineering diagnostics, not product interaction. For external use, it is better to build separate domain endpoints or a carefully designed info.
GO TO FULL VERSION