Back to blog
Environment Variables in Java: System.getenv(), Spring Boot @Value, and Best Practices

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.

February 16, 2026by Patrick Gerrits
environment-variablesjavaspring-bootconfiguration

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.

AspectSystem.getenv()System.getProperty()
SourceOperating system environment variablesJVM system properties
Set viaShell export, Dockerfile ENV, process manager-D flags at JVM startup
ScopeInherited from OS/shellJVM-specific
Exampleexport DB_URL=jdbc:...java -Ddb.url=jdbc:... Main
MutabilityRead-only after JVM startsCan 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:

  1. JVM system properties (-D flags)
  2. Operating system environment variables
  3. application.properties or application.yml files
  4. 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 configuration
  • application-dev.properties - development overrides
  • application-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):

  1. Command-line arguments (--server.port=9000)
  2. JVM system properties (-Dserver.port=9000)
  3. OS environment variables (SERVER_PORT=9000)
  4. Profile-specific properties (application-prod.properties)
  5. 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.

Try EnvManager free →

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.

Ready to manage your environment variables securely?

EnvManager helps teams share secrets safely, sync configurations across platforms, and maintain audit trails.

Get started for free

Get DevOps tips in your inbox

Weekly security tips, environment management best practices, and product updates.

No spam. Unsubscribe anytime.