The config layer is the foundation. It answers one question: where is the API and how do I connect to it? Base URL, credentials, timeouts, feature flags — all live here. Never hardcode these values anywhere else.
# Dev Environment Configuration
base.url=http://localhost:8080/api/v1
auth.username=testuser
auth.password=testpass123
auth.token.url=http://localhost:8080/auth/token
# Timeouts (milliseconds)
connection.timeout=5000
response.timeout=10000
# Feature flags
retry.enabled=true
retry.count=2
logging.enabled=true# Staging Environment Configuration
base.url=https://staging-api.company.com/api/v1
auth.username=qa_staging
auth.password=stg_secure_456
auth.token.url=https://staging-api.company.com/auth/token
# Timeouts (milliseconds)
connection.timeout=10000
response.timeout=30000
# Feature flags
retry.enabled=true
retry.count=3
logging.enabled=falseSame keys. Different values. That is the whole idea. Your framework code reads keys. The properties file provides the values for that environment.
package com.company.api.config;
import java.io.InputStream;
import java.util.Properties;
public class ConfigReader {
private static final Properties properties = new Properties();
private static boolean loaded = false;
private ConfigReader() {
// prevent instantiation — all methods are static
}
public static void loadConfig() {
if (loaded) return;
String env = System.getProperty("env", "dev");
String filename = "config/" + env + ".properties";
try (InputStream is = ConfigReader.class.getClassLoader()
.getResourceAsStream(filename)) {
if (is == null) {
throw new RuntimeException(
"Config file not found: " + filename
+ ". Available: dev.properties, staging.properties, prod.properties"
);
}
properties.load(is);
loaded = true;
System.out.println("[ConfigReader] Loaded config: " + filename);
} catch (Exception e) {
throw new RuntimeException("Failed to load config: " + filename, e);
}
}
public static String getBaseUrl() {
loadConfig();
return properties.getProperty("base.url");
}
public static String getUsername() {
loadConfig();
return properties.getProperty("auth.username");
}
public static String getPassword() {
loadConfig();
return properties.getProperty("auth.password");
}
public static String getTokenUrl() {
loadConfig();
return properties.getProperty("auth.token.url");
}
public static int getConnectionTimeout() {
loadConfig();
return Integer.parseInt(
properties.getProperty("connection.timeout", "5000")
);
}
public static int getResponseTimeout() {
loadConfig();
return Integer.parseInt(
properties.getProperty("response.timeout", "10000")
);
}
public static boolean isRetryEnabled() {
loadConfig();
return Boolean.parseBoolean(
properties.getProperty("retry.enabled", "false")
);
}
public static int getRetryCount() {
loadConfig();
return Integer.parseInt(
properties.getProperty("retry.count", "1")
);
}
public static String get(String key) {
loadConfig();
return properties.getProperty(key);
}
}Developer runs: mvn test -Denv=staging
Maven passes -Denv=staging as a system property
ConfigReader reads System.getProperty("env") and gets "staging"
ConfigReader loads config/staging.properties from classpath
BaseTest calls ConfigReader.getBaseUrl() and gets staging URL
All tests run against staging — zero code changes
Notice the private constructor and static methods. ConfigReader is a utility class — you never create an instance of it. You call ConfigReader.getBaseUrl() directly. The loadConfig() call is lazy — it loads the file on first access and caches it. Subsequent calls hit the cache.
Never commit credentials to properties files in real projects. Use environment variables or a secrets manager. For practice and learning, hardcoded values are fine. In production, read sensitive values from System.getenv("API_KEY") or a vault.
Q: How does your framework handle multiple environments?
A: We have separate properties files for each environment — dev.properties, staging.properties, prod.properties — all in src/test/resources/config/. A ConfigReader utility class reads the "env" system property to decide which file to load. The default is dev. When running tests, we pass -Denv=staging via Maven. The ConfigReader loads the right file, and all downstream code (BaseTest, endpoints) automatically gets the correct base URL, credentials, and timeouts. Zero code changes needed to switch environments.
Key Point: Config layer uses properties files per environment and a static ConfigReader class. Switch environments with -Denv=staging. Never hardcode URLs or credentials in Java code.