Why your settings don't live in your code
Every application has settings that change depending on where it runs. The database URL on your laptop is not the one in production. The port the app listens on might be 8080 locally and something else inside a container. The API key you test with is not the real one.
Externalized configuration is the simple idea that these settings live outside your compiled code — in a text file, an environment variable, or a command-line flag — so you can change them without recompiling. You write the code once; the settings travel separately and get slotted in when the app starts.
You meet this the first time you deploy a Spring Boot app. It runs fine on your machine, you ship the exact same jar to a server, and it picks up a different database — without a single line of code changing. This article is about how Spring pulls that off, and the one question that trips everyone up: when the same setting is defined in two places, who wins?
Spring's first job: build one big lookup table
Before your code runs, Spring goes hunting for settings. It looks in files, it reads environment variables, it scans the command line — and it pours everything it finds into a single key/value lookup.
Spring calls this lookup the Environment. Think of it as one flat dictionary: you ask it for a key like server.port, and it hands back a value like 8080. Every setting your app could possibly care about ends up in here, no matter where it originally came from.
The most common place to put settings is a file named application.properties, which Spring looks for automatically:
server.port=8080
app.greeting=Hello from the properties file
Each line is one key and one value. Once Spring has read this file into the Environment, any part of your app can ask for those keys.
Reading a value: the two ways in
The quickest way to pull a value out is the @Value annotation. You put it on a field, and Spring fills that field in for you as it builds the object:
@Component
class GreetingService {
@Value("${app.greeting}")
private String greeting;
String greet() {
return greeting;
}
}
The ${...} syntax means "look this key up in the Environment." So ${app.greeting} gets replaced with Hello from the properties file. The @Component annotation just tells Spring to create and manage this object, which is what gives Spring the chance to inject the value in the first place.
That works well for one or two values. But sometimes you want to ask the Environment directly, in code. For that, Spring will hand you the Environment object itself — just declare it as a constructor parameter and Spring passes it in:
@Component
class GreetingService {
private final String greeting;
GreetingService(Environment env) {
this.greeting = env.getProperty("app.greeting");
}
}
Same result, read a different way. env.getProperty("app.greeting") returns the value, or null if the key was never defined anywhere. Both styles read from the same Environment — the same big lookup table. That matters for everything that follows, because the interesting question isn't how you read a key, it's what value the table holds when two sources disagree.
The problem: the same key, defined twice
Here is where it gets real. Say your application.properties sets the port:
server.port=8080
But when you launch the app on a server, you also pass a command-line flag:
java -jar app.jar --server.port=9000
Now server.port is defined in two places — the file says 8080, the command line says 9000. Only one value can end up in the Environment. Which one does your app actually use?
The answer is 9000. And that is not an accident or a coincidence — it is a deliberate, fixed set of rules called property source precedence.
Property sources and their pecking order
Each place Spring pulls settings from is called a property source. The application.properties file is one property source. Environment variables are another. Command-line arguments are a third. There are more, but those three are the ones you meet daily.
Spring keeps every property source in a strict, ordered list. When you ask for a key, Spring walks the list from the top and returns the first value it finds. So a source higher in the list overrides the same key in any source below it.
Here is the order, from highest priority (wins) down to lowest priority (fallback), trimmed to the sources you'll actually run into:
-
Command-line arguments —
--server.port=9000 -
Java system properties —
-Dserver.port=9000 -
OS environment variables —
SERVER_PORT=9000 -
Profile-specific files —
application-prod.properties -
The main file —
application.properties - Default properties — hard-coded fallbacks set in code
Read it top to bottom as "who gets the last word." The command line beats everything. Your plain application.properties sits near the bottom — it is the baseline that everything else is allowed to override. That is exactly what you want: the file holds sensible defaults, and each deployment tweaks a few of them from the outside.
So in our example, --server.port=9000 (source 1) beats server.port=8080 in the file (source 5). The command line wins. That is the whole rule.
Walking the list, key by key
One subtlety worth pinning down: precedence is decided per key, not per source. Spring doesn't pick one winning file and ignore the rest. It resolves each key independently.
Imagine this setup. Your file:
server.port=8080
app.greeting=Hello from the file
app.timeout=30
And at launch you pass one environment variable and one command-line flag:
export SERVER_PORT=9000
java -jar app.jar --app.greeting="Hello from the command line"
Now trace each key down the list:
-
server.port→ the environment variable (source 3) sets it to9000, beating the file. Result: 9000. -
app.greeting→ the command-line flag (source 1) sets it, beating the file. Result: Hello from the command line. -
app.timeout→ only the file (source 5) has it. Nothing above overrides. Result: 30.
Three keys, three different winning sources, all resolved in the same run. That is the mental model to hold: for every key, Spring scans from the top and stops at the first hit.
(You may have noticed the environment variable was spelled SERVER_PORT — uppercase, with an underscore — while the file used server.port. Spring matches those two forms to the same key on purpose, because most operating systems won't allow dots in variable names. The exact matching rules are their own topic; for now, just know the uppercase-underscore form lines up with the dotted one.)
Profiles, briefly
You'll see one more entry sitting between environment variables and the main file: profile-specific files like application-prod.properties or application-dev.properties.
A profile is just a name for a set of circumstances — "prod," "dev," "test." When a profile is switched on, Spring loads the matching file and stacks it above the plain application.properties. So application-prod.properties can override the baseline, while anything from the command line or environment still overrides that:
# application.properties (baseline, lowest of the files)
app.greeting=Hello from dev
# application-prod.properties (wins when the "prod" profile is on)
app.greeting=Hello from production
Turning a profile on is a small topic of its own, so we'll leave the how for later. The point here is only its rung on the ladder: profile files beat the main file, and everything external beats the profile files.
Why the order is built this way
Step back and the design makes sense. The rule of thumb is: the more specific and the more external a source is, the more it wins.
Your application.properties is the most general and the most baked-in — it ships inside the jar as the default. Profile files are more specific — they apply only in certain circumstances. Environment variables and command-line flags are the most external of all — they come from whoever is launching the app, right at that moment, on that machine.
That ordering is what lets one jar behave correctly everywhere. The safe defaults live in the file. The deployment overrides just the handful of keys it needs — a real database URL, a production port — from the outside, and those overrides land on top. Nothing gets recompiled, and nothing gets edited inside the jar.
The gotcha that bites everyone
The classic production surprise is an override you forgot was there. You change server.port in application.properties, redeploy, and the port doesn't change. You stare at the file — it clearly says the new value.
The culprit is almost always a source higher up the list. A leftover SERVER_PORT environment variable on the server, or a --server.port flag baked into a startup script, is quietly winning over your file every single time. Your edit is correct; it is just being overruled by something with more precedence.
The fix follows straight from the model: when a value isn't what the file says, don't start with the file — start at the top of the list and walk down. Check the command line, then system properties, then environment variables. The first source that defines the key is the one actually deciding, and it is usually not the file you were looking at.
The one thing to carry away
Spring gathers every setting into one Environment, and resolves each key by walking an ordered list of property sources — command line, then system properties, then environment variables, then profile files, then application.properties, then coded defaults — stopping at the first source that defines it.
Higher means more external, and more external means it wins. Once that single ordering is in your head, "why is my config not taking effect?" stops being a mystery and becomes a lookup you can do in ten seconds.
Top comments (0)