🧱 Configuration Manager — Singleton (System Design Question — Solution)
This document is a solution write-up for the system design question: implement a Configuration Manager where only one instance manages application settings. It presents the problem, a practical TypeScript implementation using the Singleton pattern, usage examples, and notes on production considerations.
Problem — Configuration Manager (Singleton use-case)
Many applications need a central place to hold configuration settings: theme, feature flags, connection strings, and user preferences. The requirements for a robust configuration manager are:
- Only a single instance must exist during the application lifecycle.
- It should provide methods to:
- Load configuration from a file or source
- Get configuration values by key
- Update configuration values in memory
- Save configuration changes back to the source
- All parts of the app should access the same configuration instance so state is consistent across modules.
Why does this map well to the Singleton pattern?
- Configuration is global-ish state — multiple instances would allow divergence.
- A single instance simplifies caching, write coordination, and lifecycle management.
- It gives a convenient global access point without relying on global variables everywhere.
Example Inputs (for the tutorial)
A simple key:value file settings.txt that looks like:
THEME: DARK
BRIGHTNESS: 64%
VOLUME: 90%
HOTSPOT: OFF
WIFI: ONA small TypeScript type to hold settings:
export type TSettings = { [key: string]: string };(You can find these in the example settings.txt and utils.ts in the exercise folder.)
Solution — TypeScript Singleton Configuration Manager
We'll implement a class ConfigManager with the following contract:
- Inputs: path to a configuration file (string) when calling
loadorinit. - Outputs: methods to get/set values, and to persist changes.
- Error modes: file-not-found, parse errors, write failures — all surfaced via thrown errors or returned promises.
Key Design Choices
- Private constructor and a static
getInstance()to enforce single instantiation. - Lazily load configuration (explicit
load()call) to keep control over I/O. - Use
fs.promisesfor asynchronous file operations. - Use a simple key:value parsing for the example; swap in JSON or YAML for production.
🧩 Implementation (Concise, Annotated)
// ConfigManager.ts
import path from "path";
import { promises as fs } from "fs";
import type { TSettings } from "./utils";
export class ConfigManager {
private settings: TSettings = {};
private sourcePath = "";
private static instance: ConfigManager | null = null;
// Private ctor prevents `new ConfigManager()` from outside
private constructor() {}
// Global access point
public static getInstance(): ConfigManager {
if (!ConfigManager.instance) {
ConfigManager.instance = new ConfigManager();
}
return ConfigManager.instance;
}
// Load configuration from a simple key:value file (or switch to JSON/YAML)
public async load(filePath: string): Promise<void> {
this.sourcePath = path.resolve(filePath);
const raw = await fs.readFile(this.sourcePath, "utf8");
this.settings = this.parseKeyValue(raw);
}
public get(key: string, fallback?: string): string | undefined {
return this.settings[key] ?? fallback;
}
public getAll(): TSettings {
// return a shallow copy to avoid accidental external mutation
return { ...this.settings };
}
public set(key: string, value: string): void {
this.settings[key] = value;
}
public async save(): Promise<void> {
if (!this.sourcePath) throw new Error("No source path configured. Call load(path) first.");
const data = Object.entries(this.settings)
.map(([k, v]) => `${k}: ${v}`)
.join("\n");
await fs.writeFile(this.sourcePath, data, "utf8");
}
private parseKeyValue(raw: string): TSettings {
const out: TSettings = {};
raw.split(/\r?\n/).forEach((line) => {
const trimmed = line.trim();
if (!trimmed) return;
const parts = trimmed.split(/:\s*/);
if (parts.length >= 2) {
const key = parts.shift()!.trim();
const value = parts.join(":").trim();
out[key] = value;
}
});
return out;
}
}🧠 Usage Example
import { ConfigManager } from "./ConfigManager";
async function main() {
const cfg = ConfigManager.getInstance();
await cfg.load("./settings.txt");
console.log("Theme:", cfg.get("THEME")); // DARK
cfg.set("BRIGHTNESS", "70%");
console.log("Brightness (mem):", cfg.get("BRIGHTNESS"));
await cfg.save(); // writes change back to settings.txt
}
main().catch(console.error);Notes, Edge-Cases & Alternatives
- Thread-safety: Node/JS uses a single-threaded event loop, so classic locking is rarely needed.
If your app uses worker threads or multiple processes, coordinate access (e.g., via IPC, a central service, locks, or a database). - Initialization order: Prefer explicit
await cfg.load(path)early in app startup.
Avoid lazy implicit loads sprinkled across modules. - Serialization format: For complex settings prefer JSON, YAML, or TOML rather than ad-hoc key:value lines.
- Testing: Because access is global, tests should reset the singleton state between tests.
You can add a non-exported reset method behind a test-only flag, or designConfigManagerto accept an in-memory source for tests. - Persistence and atomicity: When saving, consider writing to a temp file then renaming for atomic updates.
✅ Conclusion
The Singleton pattern is a great fit when you need a single, centralized instance that manages state across an application — like a configuration manager.
The TypeScript example above demonstrates a simple, clear implementation you can adapt for production by switching to JSON/YAML, adding error handling and tests, and addressing multi-process concerns.
Read more
System Design Question — Resume Builder (Builder) — Solution
Solution: Implement a Resume Builder in TypeScript — problem statement, implementation, usage, and notes.
Creational Design Patterns Explained with Real-World Examples
Understand the five key creational design patterns — Singleton, Factory Method, Abstract Factory, Builder, and Prototype — using real-world coding scenarios.
System Design Question — Enemy Cloner (Prototype) — Solution
Solution: Implement a Game Enemy Cloning System in TypeScript — problem statement, implementation, usage, and notes.