Java reads environment variables through System.getenv(), which returns an unmodifiable Map<String, String>. Once the JVM starts, that map is read-only — there is no portable API to set or remove a variable from inside the process. That immutability is the central gotcha for developers coming from Node.js or Python: you cannot "patch" the environment at runtime, so configuration must be sourced before the JVM starts or layered through a higher-level abstraction. In modern Spring Boot apps, that abstraction is IConfiguration — sorry, wrong runtime. In Spring Boot it is the Environment / @Value machinery, which automatically maps SPRING_DATASOURCE_URL to spring.datasource.url via relaxed binding. This guide covers the primitive APIs, the Spring Boot mapping rules, and the dotenv-java escape hatch for non-Spring projects.
How do you read an environment variable in Java?
Two methods on java.lang.System matter: getenv(String) for a single variable and getenv() for the whole map. Both return null for unset keys — guard with Optional or Map.getOrDefault to avoid NullPointerException.
// Single value — returns null if unset
String dbUrl = System.getenv("DATABASE_URL");
// With a default
String port = System.getenv().getOrDefault("PORT", "3000");
// Optional-style guard
String apiKey = Optional.ofNullable(System.getenv("API_KEY"))
.orElseThrow(() -> new IllegalStateException("API_KEY not set"));
// Whole map — unmodifiable
Map<String, String> all = System.getenv();
all.forEach((k, v) -> System.out.println(k + "=" + v));Environment variables vs JVM system properties
Java has a second, separate namespace: system properties, accessed via System.getProperty and set with the -D flag. Many libraries (especially older ones) read system properties rather than environment variables — Java logging, the SSL truststore, and almost everything in the java. namespace.
# Environment variable
export DATABASE_URL=postgres://localhost/mydb
java -jar app.jar
# System property — same value reached via System.getProperty("database.url")
java -Ddatabase.url=postgres://localhost/mydb -jar app.jar
# Java built-in system properties worth knowing
java.version # JVM version
java.home # JRE installation dir
user.home # User home directory
file.separator # / or \
line.separator # \n or \r\n
os.name # "Linux", "Mac OS X", "Windows 11"When debugging configuration issues, check both: a missing value might be in the wrong namespace, not actually unset.
How does Spring Boot bind environment variables?
Spring Boot's relaxed binding automatically maps environment variables to property names. Underscores become dots, uppercase becomes lowercase, and the resulting property is available everywhere Spring's configuration system reaches — @Value, @ConfigurationProperties, application.yml placeholders, and the Environment bean.
# Environment variable Spring property
SPRING_DATASOURCE_URL → spring.datasource.url
SPRING_DATASOURCE_USERNAME → spring.datasource.username
SERVER_PORT → server.port
LOGGING_LEVEL_ROOT → logging.level.root
MYAPP_FEATURE_NEW_UI_ENABLED → myapp.feature.new-ui.enabled// Inject via @Value
@Value("${spring.datasource.url}") String dbUrl;
// Or as a typed bean
@ConfigurationProperties(prefix = "myapp")
public record AppConfig(
String name,
int maxConnections,
Duration timeout
) {}
# In application.yml — defaults baked in, env vars override
myapp:
name: my-app
max-connections: 100
timeout: 30sSpring's precedence order (loosely): command-line args → JNDI → JVM system properties → OS environment variables → application-{profile}.yml → application.yml → defaults. Higher sources override lower ones.
How do you load a .env file in Java?
Spring Boot doesn't read .env files natively. The community standard for non-Spring apps is dotenv-java, which exposes its own API rather than mutating System.getenv() (since the latter is read-only):
// Maven: io.github.cdimascio:dotenv-java:3.0.0
import io.github.cdimascio.dotenv.Dotenv;
Dotenv dotenv = Dotenv.configure()
.directory("./config")
.ignoreIfMalformed()
.ignoreIfMissing()
.load();
String dbUrl = dotenv.get("DATABASE_URL");
String port = dotenv.get("PORT", "3000");For Spring Boot, use spring-dotenv as a build-time plugin or a custom PropertySource that reads .env at startup.
Practical patterns and gotchas
- The environment is immutable from inside the JVM. There is no standard
System.setenv. If you genuinely need runtime mutation for a test, hack it with reflection (Linux/Mac) or JNI — but most of the time you actually want a config object you control. - JAVA_OPTS and JAVA_TOOL_OPTIONS let containers pass JVM flags via the environment without changing the entrypoint.
JAVA_TOOL_OPTIONSis honoured by every JVM tool, includingjstackandjcmd. - Spring Boot relaxed binding handles kebab-case:
MYAPP_FEATURE_NEW_UIbinds tomyapp.feature.new-ui. Use@ConfigurationPropertieswith kebab-case property names; Spring translates automatically. - Container env-only deployment: in modern container orchestrators, JVM apps usually skip config files entirely and rely on environment variables injected by Kubernetes ConfigMaps / Secrets, AWS Parameter Store, or Azure Key Vault. See the env-vars tips guide for Kubernetes patterns.
- Validate your .env file before committing with the .env validator.
For Java's static final and enum constants, see constants in Java.