← Application Building
Guided build 7 checkpoints · 6 languages

Configuration precedence

One setting, several places it can come from.

A setting can come from several places. Use the first place, in a fixed order, where the setting is present. In this program’s policy, a setting that is set counts even when its value is empty. Then check that value. If it is not usable, stop with an error that names the place it came from; do not move on to the next place.

Before you begin

Why this matters

When a program runs with the wrong setting, the first question is: where did this value come from? If you can follow the order in which your code looks, you can answer that by reading it, and you can spot the lines that replace what someone typed without reporting an error.

What this helps you reason about

  • Predict which place a setting comes from when several disagree.
  • Spot a catch block that turns a bad value into the default.
  • Throw an error that says which place held the bad value.

A program often needs one setting, such as a port number, that can come from several places. This lesson builds the part of a Java program that picks one value, checks it, and says where it came from.

Already know this? The short version
  • Take the setting from the highest source that is present, here flag, then environment variable, then file, then the default.
  • null is absent and "" is present; System.getenv returns "" for a variable set to the empty string, and getOrDefault keeps it.
  • Check only the chosen text, with your own rule (here 1 to 5 ASCII digits, then 1 to 65535), because Integer.parseInt accepts "+80" and digits from other scripts, such as "80".
  • An invalid winner is an error that names its source and text; a catch block that substitutes the default hides it.
  • Read args, System.getenv, and files in one place, at the start of the program, and pass their text to a method that reads nothing itself.

Go to the final check· Go to the practice tickets

Words in this lesson (8)
setting
One value a program needs before it can do its work, such as the port number it listens on.
source
A place a setting can come from: a command-line flag, an environment variable, a configuration file, or a default written in the program.
precedence
The fixed order that decides which source wins when several supply the same setting. In this lesson: flag, then environment variable, then file, then default.
present and absent
A source is present when it supplies a value for the setting, even an empty one. It is absent when it supplies nothing at all.
null
A reference that refers to no object. Map.get and System.getenv return null for a name that is not there.
exception
An object that is thrown to stop the normal flow when an operation fails, such as the NumberFormatException from Integer.parseInt. A catch block can handle it.
validation
Checking that a value follows the rules the program needs, such as “1 to 5 digits, from 1 to 65535”, before using it.
provenance
A record of where a value came from: here, the name of the source that won, kept with the value and named in every error.
Build brief · one port setting
Input
The text of a flag, an environment variable, and a file line, each of which may be absent.
Output
A port number and the source it came from, or an error that names the source and its text.
Policy
flag, then env, then file, then the default 3000. In this program’s policy, a source that is set counts even when its value is empty. Only the winner is checked: 1 to 5 digits, 1 to 65535. An invalid winner is an error.

Examples are focused snippets; imports, enclosing functions, or classes may be omitted. C examples use C11; Haskell examples use Haskell 2010. Expand the notes for language-specific depth.

01 / Precedence

Which source wins?

A setting is one value a program needs before it can do its work, such as the port number it listens on. A setting can come from several sources: a command-line flag such as --port=7000, typed after the program’s name; an environment variable such as APP_PORT, set before the program starts; a line in a configuration file; or a default written in the program.

When several sources supply the port, the program needs a rule. Precedence is that rule: a fixed order of sources in which the first one that supplies a value wins. This program uses a common order: flag, then environment variable, then file, then the default 3000.

For now, the sources are String parameters, and null means the source is absent.

Main.java
static String choose(String flag, String env, String file) {
    if (flag != null) return flag + " from flag";
    if (env != null) return env + " from env";
    if (file != null) return file + " from file";
    return "3000 from default";
}

Look at the sources in precedence order and stop at the first one that supplies a value.

Precisely: the order is a choice

No language or operating system sets this order. Flag, then environment, then file, then default is a common convention, because each source is more specific to one run than the next. Some tools add more sources, such as a file per user and a file per project. Whatever the order, a program should write it down where its users can read it.

Experiment

Follow the sources from the highest

choose tests the sources in precedence order, highest first, and returns at the first one that is present. An absent source is skipped. Once a source is found, the lower ones are never looked at.

Why it matters When a setting surprises you, follow the same order by hand. The first source that is present is the one that won.

Predict which source wins before running. Then change "5000" in the call to null, so the environment variable is absent too, and predict again.

class Main {
    static String choose(String flag, String env, String file) {
        if (flag != null) return flag + " from flag";
        if (env != null) return env + " from env";
        if (file != null) return file + " from file";
        return "3000 from default";
    }

    public static void main(String[] args) {
        String port = choose(null, "5000", "4100");
        System.out.println(port);
    }
}
What to look for

Step into choose. flag shows null, so the first if does not return; env shows "5000", so the second one does. Back in main, port shows "5000 from env". After the edit, env shows null too, the third if returns, and the run prints 4100 from file.

Expected output

5000 from env

02 / Presence

Is an empty value the same as a missing one?

A source is present when it supplies a value, and absent when it supplies nothing. Starting a program with APP_PORT set to "" sets the variable, to an empty value. In this program’s policy, a source that is set counts even when its value is empty: someone set it, so the program checks that empty value and reports an error, instead of moving on to the next source. Some tools choose the other policy and treat an empty variable as not set; either is fine if the documentation says which.

System.getenv("APP_PORT") returns null when the variable is not set, and its text when it is, even "". System.getenv() with no argument returns every variable as a Map<String, String>. On a Map, get returns null for a missing key, and getOrDefault gives the default only when the key is missing, so it keeps "".

Main.java
Map<String, String> env = System.getenv();
String port = env.get("APP_PORT"); // null if missing
String chosen = env.getOrDefault("APP_PORT", "3000"); // keeps ""

Test for null to decide whether a source is present. "" is a value.

Coming from JavaScript: Java has no falsy values: "" is not null, and calling isEmpty() on null throws a NullPointerException.

Precisely: the environment cannot be changed

Java has no method that sets an environment variable of the running program; the Map from System.getenv() cannot be modified. That is why the experiment builds its own Map with Map.of instead of setting APP_PORT.

Experiment

Tell an empty value from a missing one

Map.get returns null for a missing key and the stored value otherwise, even "". getOrDefault gives the default only when the key is missing. The Map that System.getenv() returns behaves the same way.

Why it matters Test for null to decide whether a source is present. Leave an empty value for the check, which can report it.

Predict both values in brackets before running. Then change Map.of("APP_PORT", "") to Map.of() and predict again.

import java.util.Map;

class Main {
    public static void main(String[] args) {
        // Stands in for System.getenv(), which also returns a Map.
        Map<String, String> env = Map.of("APP_PORT", "");
        String port = env.get("APP_PORT");
        String chosen = env.getOrDefault("APP_PORT", "3000");
        System.out.println("[" + port + "] [" + chosen + "]");
    }
}
What to look for

Step past each line. env shows java.util.ImmutableCollections$Map1@ and an id, then its one pair, {"APP_PORT"=""}. port and chosen both show "", and the run prints [] []. After the edit, env shows an ImmutableCollections$MapN with no pairs, {}, port shows null, chosen shows "3000", and the run prints [null] [3000]. The number after @ is the debugger’s ID for the map, not a memory address.

Expected output

[] []

03 / Validation

What if the winning value is not a port?

The chosen value is still text. Before the program uses it as a port, validation checks the text against the program’s own rule: one to five ASCII digits (the characters 0 to 9), and a number from 1 to 65535. Leading zeros are allowed, so 00080 means 80.

Integer.parseInt is not that rule. It throws a NumberFormatException for "80x" and " 80", but it accepts "+80", and it accepts digits from other writing systems, such as the full-width "80". So the program checks each character against '0' and '9' first, and converts afterwards.

When the winning value fails the check, the program could fall through to the next source or stop with an error. This program stops. If the flag says 80x, the person asked for a port on this run. Using the environment’s 5000 instead, or a catch block that sets the default, with no error, would start the program on a port the person did not ask for.

Main.java
boolean digits = raw.length() >= 1 && raw.length() <= 5;
for (char c : raw.toCharArray()) {
    if (c < '0' || c > '9') digits = false;
}
if (!digits) {
    throw new IllegalArgumentException(where + " is not 1 to 5 digits");
}
int port = Integer.parseInt(raw);

Check the value that won. If it fails, throw an error; do not try the next source.

Coming from Python: NumberFormatException is unchecked, like Python’s ValueError: the compiler does not remind you to catch it.

Precisely: Character.isDigit

Character.isDigit is also true for digits from other writing systems, so it is not a test for 0 to 9. Comparing with '0' and '9' is. After this check, parseInt cannot throw, because five ASCII digits always fit in an int.

Experiment

Give the parser a value that is not a port

Integer.parseInt throws a NumberFormatException for text it cannot read. The catch block replaces the error with 3000 and carries on, so the mistake in the flag is hidden and the default appears under the flag’s name. parseInt also accepts a leading + or -.

Why it matters Catch the exception where you can report it with the source and the text. Do not turn it into a default.

Predict the output before running. Then change "80x" to "+80" and predict again.

class Main {
    static String load(String flag, String env) {
        String raw = "3000";
        String source = "default";
        if (env != null) { raw = env; source = "env"; }
        if (flag != null) { raw = flag; source = "flag"; }
        int port;
        try {
            port = Integer.parseInt(raw);
        } catch (NumberFormatException e) {
            port = 3000;
        }
        return port + " from " + source;
    }

    public static void main(String[] args) {
        System.out.println(load("80x", "5000"));
    }
}
What to look for

Step through load and watch raw and source change from the default to env and then to flag. After the parseInt line, the next stops are the catch line and the line inside it: e shows a NumberFormatException whose detailMessage is For input string: "80x". port becomes 3000, and the run prints 3000 from flag. After the edit, nothing stops in catch, port shows 80, and the run prints 80 from flag.

Expected output

3000 from flag

04 / Provenance

How does the program say where a value came from?

Provenance is a record of where a value came from. The program keeps it by returning the source with the port, and by putting the source and the raw text in every error. Then error: flag "80x" is not 1 to 5 digits tells the person which of the four places to fix.

load returns a Setting, a record: a small class declared in one line, whose fields are read with port() and source(). When the text fails the check, load throws an IllegalArgumentException whose message holds the source and the raw text, and the code that called load catches it where it can report it.

Main.java
record Setting(int port, String source) {}

try {
    Setting s = load(flag, env, file);
    System.out.println(s.port() + " from " + s.source());
} catch (IllegalArgumentException e) {
    System.out.println("error: " + e.getMessage());
}

Return the source with the value, and name the source and the raw text in every error.

Coming from Go: Java reports a failed load by throwing an exception, not by returning an error value; the catch can be far from the throw.

Precisely: showing every setting at start

Many tools can print each setting with the place it came from; git config --list --show-origin is one example. A method that already returns the source makes that a few lines of code.

05 / Real sources

Where do the real flags, variables, and files come in?

A real program reads its sources from outside. main receives the words of the command line as its String[] args, System.getenv reads an environment variable, and Files.readString reads a file.

Keep those reads in one place, at the start of the program. That code collects the text and passes it to load; load itself reads nothing from outside. Then you can call load with any combination of sources, as the experiments and the program on this page do.

The stepper on this page always starts a program without arguments, and with an environment you cannot change. So this lesson passes the sources as arguments, with null for an absent source: the same values System.getenv gives a real program.

Main.java
// The start of a real program; not run on this page.
public static void main(String[] args) {
    String flag = null;
    for (String arg : args) {
        if (arg.startsWith("--port=")) flag = arg.substring(7);
    }
    Setting s = load(flag, System.getenv("APP_PORT"), null);

Read the command line, the environment, and files in one place, and pass plain values to the code that decides.

Precisely: the command line

args holds only the words after the class name. substring(7) drops the seven characters of --port=. A real tool usually uses a library such as picocli, which also handles --port 7000 with a space. The file is passed as null here; reading one is a separate topic in this module, Files, parsing, and serialization.

06 / Try it

Can you predict the winner and the outcome?

The workbench below is a model of this lesson’s policy that runs in your browser. It does not run Java code or read your environment. It draws the sources in the reverse of this lesson’s order: flag, the highest source, is at the right end. So the winner is the source furthest to the right that is present.

Predict the result before each change: turn off flag; turn off env as well; turn flag back on and clear its text; then type broken into file while flag holds 7000.

The flag is absent, APP_PORT is set to 0080, and the file says 70000. What does the program report, and why?

Reveal the reasoning

80 from env. The environment variable is the highest source that is present. 0080 is four digits, and leading zeros are allowed, so it gives 80. The file’s 70000 would fail the range check, but the file did not win, so it is never checked.

07 / Whole program

Does the whole program follow the policy?

The complete program below joins the pieces and runs the thirteen acceptance cases, one output line each. Each case gives the flag, the environment variable, and the file, with null for an absent source. Save it as Example.java and run it with java to see the same thirteen lines.

Main.java
class Example {
    record Setting(int port, String source) {}

    static Setting load(String flag, String env, String file) {
        String source = "default";
        String raw = "3000";
        if (flag != null) {
            source = "flag";
            raw = flag;
        } else if (env != null) {
            source = "env";
            raw = env;
        } else if (file != null) {
            source = "file";
            raw = file;
        }
        String where = source + " \"" + raw + "\"";
        boolean digits = raw.length() >= 1 && raw.length() <= 5;
        for (char c : raw.toCharArray()) {
            if (c < '0' || c > '9') digits = false;
        }
        if (!digits) {
            throw new IllegalArgumentException(where + " is not 1 to 5 digits");
        }
        int port = Integer.parseInt(raw);
        if (port < 1 || port > 65535) {
            throw new IllegalArgumentException(where + " is not between 1 and 65535");
        }
        return new Setting(port, source);
    }

    public static void main(String[] args) {
        // Each case is {flag, env, file}; null means absent.
        String[][] cases = {
            {"7000", "5000", "4100"},
            {null, "5000", "4100"},
            {null, null, "4100"},
            {null, null, null},
            {"", "5000", null},
            {"80x", "5000", null},
            {" 80", null, null},
            {"+80", null, null},
            {"0", null, null},
            {"65536", null, null},
            {"7000", null, "broken"},
            {"00080", null, null},
            {null, "", "4100"},
        };
        for (String[] c : cases) {
            try {
                Setting s = load(c[0], c[1], c[2]);
                System.out.println(s.port() + " from " + s.source());
            } catch (IllegalArgumentException e) {
                System.out.println("error: " + e.getMessage());
            }
        }
    }
}

Acceptance cases

  • flag 7000, env 5000, file 41007000 from flag
  • no flag; env 5000, file 41005000 from env
  • only file 41004100 from file
  • no flag, env, or file3000 from default
  • flag "" (set, but empty), env 5000error: flag "" is not 1 to 5 digits
  • flag 80x, env 5000error: flag "80x" is not 1 to 5 digits
  • flag " 80" (a space first)error: flag " 80" is not 1 to 5 digits
  • flag +80error: flag "+80" is not 1 to 5 digits
  • flag 0error: flag "0" is not between 1 and 65535
  • flag 65536error: flag "65536" is not between 1 and 65535
  • flag 7000, file broken7000 from flag; the file is never checked
  • flag 0008080 from flag
  • env "" (set, but empty), file 4100error: env "" is not 1 to 5 digits

What does this program settle, and what does it leave out?

Reveal the reasoning

It settles one setting: which source wins, whether its text is a port, and how to say where it came from. It does not read a real command line, environment, or file, check settings that did not win, or start anything on the port. Each of those is a separate piece with its own rules.

Practice tickets

  1. CFG-101 · Stop hiding a bad retention setting → Free
  2. CFG-102 · Let the last width win → Sign in · Pro
  3. CFG-103 · Choose the watering minutes → Sign in · Pro

Keep exploring

I/O, buffering, and external state →

Primary references