Configuration Hierarchy — Complete Implementation Guide
In this tutorial, you will learn about Configuration Hierarchy. We cover key concepts, practical examples, and best practices to help you master this topic.
Configuration hierarchy defines the order in which configuration sources are consulted, with higher-precedence sources overriding lower-precedence ones, enabling flexible configuration across environments.
What You'll Learn
By the end of this tutorial, you will understand the standard configuration hierarchy, how to implement layered configuration loading, and how to design overridable configuration defaults.
Why It Matters
Without a clear configuration hierarchy, it's impossible to know which configuration value wins. A well-designed hierarchy makes configuration predictable and debuggable.
Real-World Use
DodaTech's configuration loading follows a 5-layer hierarchy: defaults -> config files -> .env -> environment variables -> command-line arguments. Each layer overrides the previous.
Configuration Hierarchy Learning Path
flowchart LR
A[TOML Configuration] --> B[Configuration Hierarchy]
B --> C[Layer 1: Defaults]
B --> D[Layer 2: Config Files]
B --> E[Layer 3: Env Vars]
B --> F{You Are Here}
style F fill:#f90,color:#fff
The Five-Layer Hierarchy
The standard configuration hierarchy has five layers, each with higher precedence.
class ConfigHierarchy {
static describe() {
return [
{
layer: 1,
name: "Hardcoded Defaults",
description: "Sensible defaults embedded in code",
example: "const port = 3000",
when: "Used when no other source provides a value"
},
{
layer: 2,
name: "Config Files",
description: "YAML, JSON, or TOML files",
example: "config/production.yaml",
when: "Loaded at startup, provides environment defaults"
},
{
layer: 3,
name: ".env File",
description: "Local development overrides",
example: ".env or .env.local",
when: "Development convenience, not used in production"
},
{
layer: 4,
name: "Environment Variables",
description: "OS or orchestrator-provided values",
example: "DB_HOST=prod.example.com",
when: "Primary production configuration mechanism"
},
{
layer: 5,
name: "Command-Line Arguments",
description: "Runtime overrides",
example: "--port 8080",
when: "Temporary overrides for debugging or testing"
}
];
}
}
ConfigHierarchy.describe().forEach(l => {
console.log(`Layer ${l.layer}: ${l.name}`);
console.log(` ${l.description}`);
});
// Layer 1: Hardcoded Defaults
// Layer 2: Config Files
// Layer 3: .env File
// Layer 4: Environment Variables
// Layer 5: Command-Line Arguments
Implementing Layered Configuration
A layered configuration loader that respects precedence.
class LayeredConfigLoader {
constructor() {
this.layers = [];
this.config = {};
}
addLayer(name, loader) {
this.layers.push({ name, loader });
}
load() {
for (const layer of this.layers) {
try {
const values = layer.loader();
if (values && typeof values === "object") {
const prevKeys = Object.keys(this.config);
this.config = { ...this.config, ...values };
const newKeys = Object.keys(values).filter(k => !prevKeys.includes(k));
const overriddenKeys = prevKeys.filter(k => values[k] !== undefined && this.config[k] !== undefined);
if (overriddenKeys.length > 0) {
console.log(`Layer "${layer.name}": overridden ${overriddenKeys.join(", ")}`);
}
}
} catch (err) {
console.warn(`Layer "${layer.name}": ${err.message}`);
}
}
return this.config;
}
}
const loader = new LayeredConfigLoader();
// Layer 1: Defaults
loader.addLayer("defaults", () => ({
port: 3000,
host: "localhost",
logLevel: "info",
cacheTTL: 300
}));
// Layer 2: Config file (if exists)
loader.addLayer("config-file", () => {
try {
return require("js-yaml").load(require("fs").readFileSync("./config.yaml", "utf8"));
} catch {
return {};
}
});
// Layer 3: Environment variables
loader.addLayer("env", () => ({
port: process.env.PORT ? parseInt(process.env.PORT) : undefined,
host: process.env.HOST,
logLevel: process.env.LOG_LEVEL
}));
const config = loader.load();
console.log("Final config:", config);
Environment-Specific Override Chain
Different environments need different parts of the hierarchy.
class EnvironmentConfigResolver {
constructor(options = {}) {
this.env = options.env || process.env.NODE_ENV || "development";
this.configDir = options.configDir || "./config";
}
resolve() {
const layers = this.getLayers();
let config = {};
for (const layer of layers) {
const values = this.loadLayer(layer);
config = this.deepMerge(config, values);
}
return config;
}
getLayers() {
const base = [
{ type: "defaults", path: "defaults" },
{ type: "shared", path: "shared" },
{ type: "env", path: this.env }
];
if (this.env === "development") {
base.push({ type: "local", path: "local" });
}
return base;
}
loadLayer(layer) {
const path = require("path");
const fs = require("fs");
const filePath = path.join(this.configDir, `${layer.path}.yaml`);
if (fs.existsSync(filePath)) {
return require("js-yaml").load(fs.readFileSync(filePath, "utf8"));
}
return {};
}
deepMerge(base, override) {
const result = { ...base };
for (const [key, value] of Object.entries(override || {})) {
if (value && typeof value === "object" && !Array.isArray(value)) {
result[key] = this.deepMerge(result[key] || {}, value);
} else if (value !== undefined) {
result[key] = value;
}
}
return result;
}
}
const resolver = new EnvironmentConfigResolver({ env: "production" });
const config = resolver.resolve();
console.log("Resolved config for production");
Precedence with Specificity
More specific configuration should override less specific.
class SpecificityConfig {
static resolve(allConfig) {
const entries = Object.entries(allConfig).sort((a, b) => {
return this.specificity(a[0]) - this.specificity(b[0]);
});
const result = {};
for (const [, value] of entries) {
Object.assign(result, value);
}
return result;
}
static specificity(name) {
if (name === "default") return 0;
if (name === "development") return 1;
if (name === "staging") return 2;
if (name === "production") return 3;
if (name === "local") return 4;
if (name === "override") return 5;
return 0;
}
}
const configs = {
default: { port: 3000, logLevel: "info", cache: true },
production: { port: 8080, logLevel: "warn" },
local: { logLevel: "debug" }
};
const resolved = SpecificityConfig.resolve(configs);
console.log("Resolved:", resolved);
// Resolved: { port: 8080, logLevel: "debug", cache: true }
Debugging Configuration Resolution
Logging where each config value comes from.
class DebuggableConfigLoader {
constructor() {
this.sources = new Map();
}
addSource(name, values) {
for (const [key, value] of Object.entries(values)) {
if (!this.sources.has(key)) {
this.sources.set(key, { value, source: name });
} else {
this.sources.set(key, { value, source: name });
}
}
}
getConfig() {
const config = {};
const debug = {};
for (const [key, info] of this.sources) {
config[key] = info.value;
debug[key] = `${info.value} (from ${info.source})`;
}
return { config, debug };
}
}
const loader = new DebuggableConfigLoader();
loader.addSource("defaults", { port: 3000, host: "localhost" });
loader.addSource("config.yaml", { port: 8080 });
loader.addSource("env", { host: "prod.example.com" });
const { config, debug } = loader.getConfig();
console.log("Config:", config);
console.log("Debug:", debug);
// Config: { port: 8080, host: 'prod.example.com' }
// Debug: { port: '8080 (from config.yaml)', host: 'prod.example.com (from env)' }
Common Mistakes
Loading layers in the wrong order -- Higher precedence should load later. If defaults load last, they always win, defeating the purpose.
Not logging which values are overridden -- Without visibility into which source provides each value, debugging configuration issues is extremely difficult.
Using too many layers -- Five layers is usually enough. More layers create confusion about which value wins.
Silently ignoring missing configuration -- Required values that are missing should fail at startup, not silently use a possibly wrong default.
Not considering the entire hierarchy when debugging -- When a configuration value is wrong, check every layer. The issue might be a lower-precedence value not being overridden.
Practice Questions
What is the highest precedence configuration source? Command-line arguments, followed by environment variables, then .env files, config files, and finally hardcoded defaults.
Why should configuration be loaded in order from lowest to highest precedence? So each layer can override the previous. Loading lowest first means defaults are set and then overridden by more specific sources.
How do you debug which configuration source provided a value? Log each value with its source name. Maintain a debug map that shows where each config key came from.
Challenge: Implement a configuration hierarchy that supports selective override at each layer.
class SelectiveOverrideConfig {
constructor() {
this.layers = [];
}
addLayer(name, loader) {
this.layers.push({ name, loader });
}
resolve() {
const result = {};
const provenance = {};
for (const layer of this.layers) {
const values = layer.loader();
for (const [key, value] of Object.entries(values)) {
if (value !== undefined) {
result[key] = value;
provenance[key] = layer.name;
}
}
}
return { config: result, provenance };
}
}
const sel = new SelectiveOverrideConfig();
sel.addLayer("defaults", () => ({ port: 3000, host: "localhost" }));
sel.addLayer("env", () => ({ port: process.env.PORT ? parseInt(process.env.PORT) : undefined }));
const { config, provenance } = sel.resolve();
console.log("Config:", config, "Provenance:", provenance);
FAQ
Mini Project
Build a layered configuration system with five layers (defaults, config file, .env, environment variables, command-line args), proper precedence, and debug output showing which source provided each value.
class FiveLayerConfig {
constructor() {
this.config = {};
this.sources = {};
}
load() {
this.applyLayer("defaults", { port: 3000, host: "localhost", logLevel: "info" });
this.applyLayer("config-file", this.loadConfigFile());
this.applyLayer(".env", this.loadDotenv());
this.applyLayer("environment", this.loadEnv());
this.applyLayer("cli", this.loadCLI());
return { config: this.config, sources: this.sources };
}
applyLayer(name, values) {
for (const [key, value] of Object.entries(values || {})) {
if (value !== undefined && value !== null) {
this.config[key] = value;
this.sources[key] = name;
}
}
}
loadEnv() {
return {
port: process.env.PORT ? parseInt(process.env.PORT) : undefined,
host: process.env.HOST,
logLevel: process.env.LOG_LEVEL
};
}
loadConfigFile() { try { return require("js-yaml").load(require("fs").readFileSync("./config.yaml", "utf8")); } catch { return {}; } }
loadDotenv() { return {}; }
loadCLI() { return {}; }
}
const config = new FiveLayerConfig();
const result = config.load();
console.log("Five-layer config loaded");
What's Next
Now that you understand configuration hierarchy, learn about secrets management. Then explore HashiCorp Vault.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro