
Environment Variables in Java: System.getenv(), Spring Boot @Value, and Best Practices
Complete guide to environment variables in Java. Covers System.getenv() vs System.getProperty(), Spring Boot @Value and @ConfigurationProperties, .env file loading with dotenv-java, and production best practices.
Environment Variables in Java
You push your Java application to production and it immediately crashes. The database connection fails. Your local development setup pointed to localhost:5432, but production needs a completely different host, credentials, and connection pool settings. You've hardcoded the configuration, and now you're scrambling to rebuild and redeploy.
This scenario is exactly why environment variables exist. They let you externalize configuration so your Java code runs unchanged across development, staging, and production. Different environments, same codebase.
Reading Environment Variables with System.getenv()
The most direct way to read environment variables in Java is System.getenv(). This method accesses operating system-level environment variables that were set before your JVM started.
Reading a Single Variable
public class DatabaseConfig {
public static void main(String[] args) {
String dbUrl = System.getenv("DATABASE_URL");
if (dbUrl != null) {
System.out.println("Connecting to: " + dbUrl);
} else {
System.out.println("DATABASE_URL not set");
}
}
}
System.getenv() returns null if the variable doesn't exist. Always check for null or provide a fallback:
String port = System.getenv("PORT");
int serverPort = port != null ? Integer.parseInt(port) : 8080;
Getting All Environment Variables
Call System.getenv() with no arguments to get a Map<String, String> of all variables:
Map<String, String> env = System.getenv();
env.forEach((key, value) -> {
System.out.println(key + " = " + value);
});
This is useful for debugging, but be careful logging this in production. Environment variables often contain secrets.
System.getenv() vs System.getProperty(): The Critical Distinction
This confusion trips up Java developers constantly. They look similar, but they're fundamentally different.
| Aspect | System.getenv() | System.getProperty() |
|---|---|---|
| Source | Operating system environment variables | JVM system properties |
| Set via | Shell export, Dockerfile ENV, process manager | -D flags at JVM startup |
| Scope | Inherited from OS/shell | JVM-specific |
| Example | export DB_URL=jdbc:... | java -Ddb.url=jdbc:... Main |
| Mutability | Read-only after JVM starts | Can be modified at runtime |
Here's the practical difference:
// Reading OS environment variable
String envValue = System.getenv("DATABASE_URL");
// Reading JVM system property
String propValue = System.getProperty("database.url");
// Setting a system property at runtime (possible)
System.setProperty("app.mode", "production");
// Trying to set an environment variable (NOT possible)
// System.getenv() returns an unmodifiable map
When to use which:
- Use
System.getenv()for configuration that varies by deployment environment (database URLs, API keys, feature flags) - Use
System.getProperty()for JVM-level settings (heap size, garbage collector flags) or quick runtime overrides during development
Most modern Java applications rely heavily on environment variables because they align with container-based deployments and 12-factor app principles.
Why Java Environment Variables are Read-Only
Unlike system properties, you cannot modify environment variables after the JVM starts. System.getenv() returns an unmodifiable map:
Map<String, String> env = System.getenv();
// This throws UnsupportedOperationException
env.put("NEW_VAR", "value");
If you need to pass different environment variables to a child process, use ProcessBuilder:
ProcessBuilder pb = new ProcessBuilder("java", "-jar", "app.jar");
Map<String, String> env = pb.environment();
// Modify environment for the child process only
env.put("CUSTOM_VAR", "customValue");
env.remove("UNWANTED_VAR");
Process process = pb.start();
This doesn't affect your current JVM's environment, only the spawned child process.
Spring Boot @Value Annotation with Environment Variables
Spring Boot makes working with environment variables significantly easier through the @Value annotation and its property resolution system.
Basic @Value Usage
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@Component
public class DatabaseConfig {
@Value("${DATABASE_URL}")
private String databaseUrl;
@Value("${DB_USERNAME}")
private String username;
@Value("${DB_PASSWORD}")
private String password;
public void connect() {
System.out.println("Connecting to: " + databaseUrl);
}
}
Spring automatically resolves ${DATABASE_URL} by checking multiple sources in this order:
- JVM system properties (
-Dflags) - Operating system environment variables
application.propertiesorapplication.ymlfiles- Profile-specific property files
Default Values with @Value
Provide fallback values when environment variables might not be set:
@Value("${SERVER_PORT:8080}")
private int serverPort;
@Value("${ENABLE_CACHE:false}")
private boolean cacheEnabled;
@Value("${DB_URL:jdbc:h2:mem:testdb}")
private String databaseUrl;
The syntax is ${VAR_NAME:defaultValue}. If SERVER_PORT isn't set, serverPort becomes 8080.
@ConfigurationProperties for Grouped Configuration
For complex configuration, @ConfigurationProperties is cleaner than multiple @Value annotations:
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Component
@ConfigurationProperties(prefix = "database")
public class DatabaseProperties {
private String url;
private String username;
private String password;
private int maxConnections;
// Getters and setters
public String getUrl() { return url; }
public void setUrl(String url) { this.url = url; }
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getPassword() { return password; }
public void setPassword(String password) { this.password = password; }
public int getMaxConnections() { return maxConnections; }
public void setMaxConnections(int maxConnections) {
this.maxConnections = maxConnections;
}
}
This binds environment variables like DATABASE_URL, DATABASE_USERNAME, DATABASE_PASSWORD, and DATABASE_MAX_CONNECTIONS to a single configuration object. Spring Boot automatically handles the camelCase to UPPER_SNAKE_CASE conversion.
Enable this feature in your main application class:
@SpringBootApplication
@EnableConfigurationProperties(DatabaseProperties.class)
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
Application.properties and Application.yml Interpolation
Spring Boot's property files support environment variable interpolation with the ${VAR_NAME} syntax. This lets you define property templates that pull values from the environment at runtime.
Using Environment Variables in application.properties
# application.properties
server.port=${SERVER_PORT:8080}
spring.datasource.url=${DATABASE_URL}
spring.datasource.username=${DB_USERNAME}
spring.datasource.password=${DB_PASSWORD}
app.name=MyApp
app.version=1.0.0
app.api.key=${API_KEY}
app.feature.experimental=${ENABLE_EXPERIMENTAL:false}
The interpolation happens when Spring loads these files. If DATABASE_URL is set in your environment, Spring substitutes it. Otherwise, you get an error (unless you provide a default with :).
Using Environment Variables in application.yml
# application.yml
server:
port: ${SERVER_PORT:8080}
spring:
datasource:
url: ${DATABASE_URL}
username: ${DB_USERNAME}
password: ${DB_PASSWORD}
app:
name: MyApp
version: 1.0.0
api:
key: ${API_KEY}
feature:
experimental: ${ENABLE_EXPERIMENTAL:false}
YAML format is identical conceptually, just cleaner syntax for nested properties.
Profile-Specific Files
Spring Boot supports environment-specific configuration files:
application.properties- base configurationapplication-dev.properties- development overridesapplication-prod.properties- production overrides
Activate a profile with the SPRING_PROFILES_ACTIVE environment variable:
export SPRING_PROFILES_ACTIVE=prod
java -jar myapp.jar
Or via system property:
java -Dspring.profiles.active=prod -jar myapp.jar
Property Resolution Order
Spring Boot resolves properties in this precedence (highest to lowest):
- Command-line arguments (
--server.port=9000) - JVM system properties (
-Dserver.port=9000) - OS environment variables (
SERVER_PORT=9000) - Profile-specific properties (
application-prod.properties) - Default properties (
application.properties)
This means an environment variable SERVER_PORT overrides whatever's in application.properties, but a JVM system property -Dserver.port=9000 overrides even that.
Loading .env Files in Java
For local development, many teams use .env files to avoid setting environment variables manually. Java doesn't support .env files natively, but libraries fill this gap.
Using dotenv-java
The dotenv-java library loads .env files and makes them accessible through a simple API:
Add the dependency:
<!-- Maven -->
<dependency>
<groupId>io.github.cdimascio</groupId>
<artifactId>dotenv-java</artifactId>
<version>3.0.0</version>
</dependency>
// Gradle
implementation 'io.github.cdimascio:dotenv-java:3.0.0'
Create a .env file in your project root:
DATABASE_URL=jdbc:postgresql://localhost:5432/mydb
DB_USERNAME=devuser
DB_PASSWORD=devpass123
API_KEY=sk_test_abc123
Load and use it in your Java code:
import io.github.cdimascio.dotenv.Dotenv;
public class Application {
public static void main(String[] args) {
Dotenv dotenv = Dotenv.load();
String dbUrl = dotenv.get("DATABASE_URL");
String apiKey = dotenv.get("API_KEY");
System.out.println("Database: " + dbUrl);
System.out.println("API Key: " + apiKey);
}
}
By default, dotenv-java looks for .env in the current directory. You can specify a different path:
Dotenv dotenv = Dotenv.configure()
.directory("/path/to/config")
.filename("custom.env")
.load();
Using spring-dotenv for Spring Boot
For Spring Boot projects, spring-dotenv integrates .env files directly into Spring's property resolution system:
Add the dependency:
<!-- Maven -->
<dependency>
<groupId>me.paulschwarz</groupId>
<artifactId>spring-dotenv</artifactId>
<version>4.0.0</version>
</dependency>
Create .env in your project root:
DATABASE_URL=jdbc:postgresql://localhost:5432/mydb
SERVER_PORT=8080
That's it. Spring Boot automatically loads the .env file on startup. You can now reference these variables in application.properties:
spring.datasource.url=${DATABASE_URL}
server.port=${SERVER_PORT}
Or inject them directly:
@Value("${DATABASE_URL}")
private String databaseUrl;
This approach is cleaner for Spring Boot apps because it unifies .env files with the existing property resolution system.
.env Files in Production
Don't commit .env files to version control if they contain secrets. Add .env to your .gitignore:
.env
.env.local
Most production deployments don't use .env files at all. Instead, platforms like Kubernetes, AWS ECS, and Docker Compose inject environment variables directly into containers.
Best Practices for Java Environment Variables in Production
1. Never Log Environment Variable Values
Environment variables often contain secrets. Logging them exposes credentials in log files:
// BAD - logs sensitive data
String apiKey = System.getenv("API_KEY");
logger.info("Using API key: " + apiKey);
// GOOD - logs only that it's set
String apiKey = System.getenv("API_KEY");
logger.info("API key configured: " + (apiKey != null));
If you need to verify configuration, log a masked version or just a boolean flag.
2. Use Secrets Managers for Sensitive Data
For production workloads, store secrets in dedicated secrets managers:
- AWS Secrets Manager
- HashiCorp Vault
- Azure Key Vault
- Google Secret Manager
These systems provide:
- Encryption at rest and in transit
- Audit logging for access
- Automatic rotation
- Fine-grained access control
Most have Java SDKs for runtime retrieval. Environment variables should point to secret identifiers, not the secrets themselves:
// Environment variable holds the secret name, not the value
String secretName = System.getenv("DB_SECRET_NAME");
String password = secretsManager.getSecret(secretName);
3. Follow 12-Factor App Principles
The 12-factor methodology recommends strict separation of config from code:
- Store config in environment variables
- Never commit credentials to version control
- Identical code should deploy to any environment
Your application.properties should be generic templates. Environment-specific values come from environment variables:
# Generic - checked into version control
spring.datasource.url=${DATABASE_URL}
spring.datasource.username=${DB_USERNAME}
spring.datasource.password=${DB_PASSWORD}
Each environment (dev, staging, prod) sets its own DATABASE_URL, DB_USERNAME, and DB_PASSWORD.
4. Validate Configuration on Startup
Fail fast if required environment variables are missing:
@Component
public class ConfigValidator implements ApplicationListener<ApplicationReadyEvent> {
@Value("${DATABASE_URL:#{null}}")
private String databaseUrl;
@Value("${API_KEY:#{null}}")
private String apiKey;
@Override
public void onApplicationEvent(ApplicationReadyEvent event) {
if (databaseUrl == null || databaseUrl.isEmpty()) {
throw new IllegalStateException("DATABASE_URL must be set");
}
if (apiKey == null || apiKey.isEmpty()) {
throw new IllegalStateException("API_KEY must be set");
}
}
}
This prevents your application from starting in a misconfigured state and wasting time with cryptic runtime errors later.
5. Document Required Environment Variables
Maintain a clear list of required environment variables in your project README:
## Required Environment Variables
- `DATABASE_URL` - PostgreSQL connection string
- `DB_USERNAME` - Database username
- `DB_PASSWORD` - Database password
- `API_KEY` - Third-party API key
- `SERVER_PORT` (optional, default: 8080) - HTTP server port
Better yet, provide an .env.example file developers can copy:
# .env.example
DATABASE_URL=jdbc:postgresql://localhost:5432/myapp
DB_USERNAME=postgres
DB_PASSWORD=yourpassword
API_KEY=your_api_key_here
SERVER_PORT=8080
Managing Environment Variables Across Environments
When your Java application runs across local development, staging, and production, keeping environment variables consistent is a real challenge. Developers might have different local .env files, staging might drift from production config, and tracking which variables are actually required becomes messy.
EnvManager lets teams manage and sync environment variables across environments from a single dashboard, with integrations for Vercel, Railway, and Render (free tier available). You define variables once, assign them to specific environments (dev, staging, prod), and team members pull the latest configuration. This eliminates the "works on my machine" problem caused by mismatched environment variables.
Setting Environment Variables in Different Environments
How you set environment variables depends on your deployment platform.
Docker:
# Dockerfile
ENV DATABASE_URL=jdbc:postgresql://db:5432/myapp
ENV SERVER_PORT=8080
Or via docker run:
docker run -e DATABASE_URL=jdbc:postgresql://... -e API_KEY=abc123 myapp
Kubernetes:
# deployment.yaml
apiVersion: apps/v1
kind: Deployment
spec:
containers:
- name: myapp
image: myapp:latest
env:
- name: DATABASE_URL
value: jdbc:postgresql://db:5432/myapp
- name: API_KEY
valueFrom:
secretKeyRef:
name: api-credentials
key: api-key
AWS Elastic Beanstalk:
Set environment variables in the Elastic Beanstalk console under Configuration > Software, or via .ebextensions:
# .ebextensions/environment.config
option_settings:
- option_name: DATABASE_URL
value: jdbc:postgresql://...
- option_name: API_KEY
value: abc123
Linux/macOS Shell:
export DATABASE_URL=jdbc:postgresql://localhost:5432/mydb
export API_KEY=sk_test_abc123
java -jar myapp.jar
For persistent variables, add them to ~/.bashrc or ~/.zshrc.
Environment variables are the backbone of configurable, portable Java applications. Whether you're using raw System.getenv(), Spring Boot's @Value annotations, or .env file loaders, the goal is the same: externalize configuration so your code stays clean and deployable anywhere. Master these patterns, and you'll never hardcode another database URL again.
Related Articles
- How to Set Environment Variables - Complete guide to setting environment variables across operating systems
- Environment Variables in Python - Using os.getenv(), python-dotenv, and environment variables in Python
- How to Set Environment Variables in Linux - Export, .bashrc, systemd, and persistent environment variables in Linux
- Git Environment Variables - Identity, SSH, debugging, and CI/CD usage
- Why .env Files Are a Security Nightmare - Security risks and how to fix them