Environment Variables and System Properties
Read settings from environment variables and -D system properties, know which wins and why, and avoid the getInteger and getBoolean traps.
String v = System.getenv("PORT"); // env var, null if missing
String p = System.getProperty("app.port", "8080"); // -Dapp.port=9000
int port = Integer.parseInt(p); // NOT Integer.getInteger(p)A Java process has two built-in sources of outside settings. Environment variables belong to the operating system process: DATABASE_URL, PORT. Containers, Kubernetes, systemd and CI all set them, which makes them the standard way to configure a deployed service. System properties belong to the JVM: -Dapp.port=9000 on the command line, read with System.getProperty. They are handy for local runs and tests.
String url = System.getenv("DATABASE_URL"); // null if not set
String port = System.getProperty("app.port", "8080"); // default if not set
Rules worth knowing
System.getenv(name)returnsnullwhen the variable is missing.System.getenv()returns an unmodifiable map: you cannot set an environment variable for the current process from Java. A child process gets its own editable copy throughProcessBuilder.environment().- Variable names are case-sensitive on Linux and macOS and case-insensitive on Windows. Stick to
UPPER_SNAKE_CASE. -Doptions go before the main class or-jar app.jar. Anything after the jar is a program argument inargs, andSystem.getPropertynever sees it.- System properties can be changed at runtime with
System.setProperty, which is convenient in tests and dangerous in production, since every class in the JVM shares them.
The two traps in the standard library
Integer.getInteger("8080") does not parse a string. It reads the system property named 8080 and returns null. Boolean.getBoolean("true") is the same: it looks up a property called true and returns false. To parse text use Integer.parseInt and Boolean.parseBoolean. Note also that Boolean.parseBoolean treats anything except true (any case) as false, so a typo such as ture quietly disables a feature.
Precedence
A common layering is: system property, then environment variable, then a default in code. The property wins because it is the most specific and easiest to change for one run. Spring Boot uses a similar order and maps names between the two styles, so SERVER_PORT in the environment sets server.port.
Testability
Reading System.getenv deep inside a class makes it hard to test, because a test cannot change the environment of its own JVM. Read once at startup into a Map or a config record and pass that in. Tests then supply a plain Map.of(...).
Two variables are read by the JVM itself: JAVA_TOOL_OPTIONS and, for the java launcher, JDK_JAVA_OPTIONS. Both add JVM options without changing the start command, which is useful in container images.
Example
When to use it
- Read DATABASE_URL and PORT from the environment in a container, where Kubernetes or Docker Compose sets them per deployment.
- Override one setting for a single local run with -Dapp.feature.search=true without editing any file.
- Give a spawned worker process its own APP_MODE while removing secrets it does not need from its environment.
More examples
getInteger and getBoolean do not parse
The getter methods take a property name, so passing a value looks it up and finds nothing. They are fine for reading -D flags by name; parseInt and parseBoolean are the tools for text, and parseBoolean turns a typo into false without complaint.
void main() {
IO.println(Integer.getInteger("8080"));
IO.println(Integer.parseInt("8080"));
IO.println(Boolean.getBoolean("true"));
IO.println(Boolean.parseBoolean("true"));
IO.println(Boolean.parseBoolean("ture"));
System.setProperty("app.cache.enabled", "true");
System.setProperty("app.port", "9000");
IO.println(Boolean.getBoolean("app.cache.enabled"));
IO.println(Integer.getInteger("app.port", 8080));
IO.println(Integer.getInteger("app.missing", 8080));
}
// Output:
// null
// 8080
// false
// true
// false
// true
// 9000
// 8080The environment is read-only, a child's copy is not
System.getenv() is an unmodifiable snapshot, so the put throws. ProcessBuilder starts with a copy of the parent's environment that you may edit before start(); the process is never started here.
void main() {
try {
System.getenv().put("APP_MODE", "test");
} catch (UnsupportedOperationException e) {
IO.println("cannot change this process's environment");
}
ProcessBuilder worker = new ProcessBuilder("java", "-jar", "worker.jar");
Map<String, String> childEnv = worker.environment();
childEnv.put("APP_MODE", "batch");
childEnv.remove("DATABASE_PASSWORD"); // do not hand secrets to a child that does not need them
IO.println("child APP_MODE = " + childEnv.get("APP_MODE"));
IO.println("child has DATABASE_PASSWORD: " + childEnv.containsKey("DATABASE_PASSWORD"));
}
// Output:
// cannot change this process's environment
// child APP_MODE = batch
// child has DATABASE_PASSWORD: falseParsing and validating a numeric setting
Environment values are always strings and often have stray whitespace from YAML or shell quoting. Parsing, trimming and range-checking at startup turns a bad value into a clear message naming the variable, instead of a failure later.
int intSetting(Map<String, String> env, String name, int fallback, int min, int max) {
String raw = env.get(name);
if (raw == null || raw.isBlank()) return fallback;
int value;
try {
value = Integer.parseInt(raw.strip());
} catch (NumberFormatException e) {
throw new IllegalArgumentException(name + " must be a whole number, got '" + raw + "'");
}
if (value < min || value > max) {
throw new IllegalArgumentException(name + " must be between " + min + " and " + max + ", got " + value);
}
return value;
}
void main() {
List<Map<String, String>> envs = List.of(
Map.of(), Map.of("PORT", " 9000 "), Map.of("PORT", "80a"), Map.of("PORT", "70000"));
for (Map<String, String> env : envs) {
try {
IO.println("PORT -> " + intSetting(env, "PORT", 8080, 1, 65535));
} catch (IllegalArgumentException e) {
IO.println("startup error: " + e.getMessage());
}
}
}
// Output:
// PORT -> 8080
// PORT -> 9000
// startup error: PORT must be a whole number, got '80a'
// startup error: PORT must be between 1 and 65535, got 70000
Discussion