Configuration via constructor, not global state
Why it matters
Global init() or configure() functions that mutate module-level state break every scenario with multiple concurrent consumers: test suites that need different API keys per test, monorepo apps using the same package with different configurations, and multi-tenant servers handling requests with per-user credentials. ISO 25010 testability and modifiability both require that configuration be isolatable per instance. Module-level state also prevents the garbage collector from cleaning up instances and leaks across test runs.
Severity rationale
Low because the limitation only surfaces when multiple configurations are required in one process — a constraint many simple consumer apps never hit — but the fix is cheap and the pattern is easily corrected.
Remediation
Replace global configuration state with a constructor or factory function that captures config in closure.
// Before — global state:
let globalConfig: Config
export function init(config: Config) { globalConfig = config }
export function doSomething() { /* uses globalConfig */ }
// After — per-instance via factory:
export function createClient(config: Config) {
return {
doSomething() { /* uses config from closure */ }
}
}
This pattern allows test suites to create isolated instances (const clientA = createClient({ apiKey: 'test-key-a' })) and monorepo apps to configure multiple instances independently. All configuration options should have TypeScript types and documented defaults.
Detection
-
ID:
config-pattern -
Severity:
low -
What to look for: Enumerate every configuration option accepted by the SDK. For each option, check how the package is configured by consumers. Look for:
- Constructor-based:
new Client({ apiKey, baseUrl })— configuration is per-instance - Factory function:
createClient({ apiKey, baseUrl })— same, just a different style - Global configuration:
SDK.configure({ apiKey })followed bySDK.doSomething()— mutable shared state - Module-level state:
setApiKey('...')that modifies module-scoped variables - Environment variable reading at import time:
process.env.API_KEYread at module load
- Constructor-based:
-
Pass criteria: The primary configuration mechanism is per-instance (constructor or factory function). Consumers can create multiple instances with different configurations. Environment variable reading is acceptable if it's done inside the constructor/factory (lazy), not at module load time — 100% of configuration options must have sensible defaults and TypeScript types. Report: "X config options found, all Y have typed defaults."
-
Fail criteria: The only configuration mechanism is a global
configure()orinit()function that sets module-level state. Multiple consumers in the same process cannot use different configurations. -
Skip (N/A) when: The package has no configuration — it's a pure utility library with no state, or configuration is entirely through function parameters (e.g.,
format(value, options)). -
Detail on fail:
"Configuration is via a module-global SDK.init({ apiKey }) that sets module-level state. This means only one configuration can exist per process — breaking in monorepos, test suites, and multi-tenant applications." -
Remediation: Per-instance configuration is more flexible and testable than global state.
// Before — global state: let globalConfig: Config export function init(config: Config) { globalConfig = config } export function doSomething() { /* uses globalConfig */ } // After — per-instance: export class Client { constructor(private config: Config) {} doSomething() { /* uses this.config */ } } // Or factory function: export function createClient(config: Config) { return { doSomething() { /* uses config from closure */ } } }
External references
- iso-25010:2011 · maintainability.modifiability — Modifiability — per-instance config avoids global mutable state
- iso-25010:2011 · maintainability.testability — Testability — per-instance config allows isolated test setups
Taxons
History
- 2026-04-18·v1.0.0·Initial import from sdk-package-quality·automated