Files
mercadodevida/legacy/specs-old/F-004-config-flags/DESIGN.md
2026-08-17 22:23:10 +02:00

3.7 KiB

DESIGN — F-004 Typed config and feature flags

Layering

  • src/infrastructure/config/config.ts — pure config loader. Owns env parsing and validation. No side effects on process.env; takes an env object so tests are deterministic. Throws a single ConfigError listing every problem found.
  • src/modules/flags/ — the feature-flag module. Business-facing, boundary-checked. Exposes only index.ts. Contains the FeatureFlagProvider interface and a simple in-memory store. Does NOT depend on infrastructure/config (kept independent so it can later be backed by a DB/external service without touching config).

Config loader (infrastructure)

export interface AppConfig {
  nodeEnv: 'development' | 'test' | 'production';
  port: number;
  host: string;
  logLevel: string;
  databaseUrl: string;      // required
  redisUrl?: string;        // optional
  flags: Record<string, boolean>; // parsed FLAG_* vars (initial seed)
}
export class ConfigError extends Error { problems: string[] }
export function loadConfig(env: StringRecord): AppConfig
  • DATABASE_URL required: missing -> ConfigError naming the var.
  • PORT parsed to int, validated 1..65535.
  • NODE_ENV whitelisted, default development.
  • FLAG_<NAME> vars: value true/false (case-insensitive) -> boolean; other values -> ConfigError naming the var. Names lowercased after stripping FLAG_ prefix.
  • Accumulate all problems, then throw once (fail fast AND clear).

Feature flag module

export interface FeatureFlagProvider {
  isEnabled(name: string): boolean;
}
export class InMemoryFeatureFlagStore implements FeatureFlagProvider {
  constructor(initial?: Record<string, boolean>)
  isEnabled(name): boolean        // unknown flag -> false (fail-safe default)
  setEnabled(name, enabled): void // runtime mutation, no redeploy needed
}
export function createFlagStore(initial?): InMemoryFeatureFlagStore
  • Unknown flags are OFF by default (fail-safe: risky paths stay skipped).
  • setEnabled mutates live state: deployment ≠ activation.
  • Guarded-path helper kept trivial; consumers do if (flags.isEnabled('x')) { ... }.

Wiring (composition root)

  • src/app/build-app.ts stays DB-free for now; but buildApp accepts an optional flags dep so routes can guard paths. Default: empty store (all flags off).
  • src/infrastructure/http/server.ts calls loadConfig(process.env), uses typed port/host, and passes config.flags into the app. On ConfigError it logs the problems and exits non-zero.
  • pool.ts keeps createPoolFromEnv but gains an optional typed overload note; no breaking change needed this ticket (config owns DATABASE_URL validation at startup).

Boundary notes

  • flags module: only imports from shared (currently nothing needed). It must not import infrastructure/config. Config parses FLAG_* into a plain record and hands it to the module at composition time — inversion keeps the module clean.
  • Boundary checker will enforce this automatically.

Files

File Role
src/infrastructure/config/config.ts typed loader + ConfigError
src/infrastructure/config/tests/config.test.ts loader unit tests
src/modules/flags/index.ts module public API
src/modules/flags/domain/feature-flag-store.ts interface + store
src/modules/flags/tests/feature-flag-store.test.ts store unit tests
src/app/build-app.ts optional flags dep injection
src/infrastructure/http/server.ts loadConfig + typed listen + ConfigError exit

Risks / mitigations

  • Silent flag default: unknown -> false, documented; tested.
  • ConfigError leaking secrets: messages name var NAMES only, never values.
  • Boundary violation (flags importing config): prevented by composition-time injection + checker.