# Configuration via constructor, not global state

- **Pattern:** `ab-002390` (`sdk-package-quality.api-surface.config-pattern`)
- **Severity:** low
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-002390
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-002390)

## 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.

```ts
// 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:
  1. Constructor-based: `new Client({ apiKey, baseUrl })` — configuration is per-instance
  2. Factory function: `createClient({ apiKey, baseUrl })` — same, just a different style
  3. Global configuration: `SDK.configure({ apiKey })` followed by `SDK.doSomething()` — mutable shared state
  4. Module-level state: `setApiKey('...')` that modifies module-scoped variables
  5. Environment variable reading at import time: `process.env.API_KEY` read at module load
- **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()` or `init()` 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.

  ```ts
  // 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 maintainability.modifiability
- iso-25010 maintainability.testability

Taxons: code-quality, user-experience

HTML version: https://auditbuffet.com/patterns/ab-002390
