skills/practices/config-management/SKILL.md
Centralized config and lightweight dependency injection — single config module, validated at startup, factory/constructor injection for wiring, composition root pattern. No heavy DI frameworks unless the app actually needs them.
npx skillsauth add devjarus/coding-agent config-managementInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Two related concerns: where config comes from, and how dependencies are wired. Both follow the same principle — centralize and inject, don't scatter and import.
process.env / os.environ / System.getenv is accessed in exactly ONE file — the config module. Everything else receives config via injection.
.env gitignored, .env.example committedconfig.database.url, config.auth.jwtSecretconfig.isProduction not process.env.NODE_ENV === 'production'For framework-specific config patterns, see rules/patterns-by-framework.md.
1. Define interfaces/protocols for dependencies
2. Accept them via constructor/factory parameters
3. Wire everything in ONE place (composition root)
4. For tests, pass fakes directly — no framework needed
5. Config is just another dependency
| Complexity | Pattern | Example | |------------|---------|---------| | Simple (< 15 services) | Factory functions + constructor injection | Most apps start here | | Medium (15-50 services) | Composition root with grouped factory functions | Growing apps | | Complex (50+ services, multiple entry points) | Lightweight container (awilix, Wire, Dagger) | Large apps, monorepos | | Framework provides it | Use the framework's DI (FastAPI Depends, Spring, SwiftUI Environment) | When it's idiomatic |
Default: start with factory functions. Add a container only when the composition root becomes painful.
One file/function that wires all dependencies. The ONLY place that knows about concrete implementations.
src/
├── composition-root.ts ← wires everything, knows all concrete classes
├── config.ts ← reads env vars, exports typed config
├── services/
│ ├── user-service.ts ← accepts interfaces, knows nothing about DB/config
│ └── order-service.ts
├── repositories/
│ ├── user-repo.ts ← implements interface, accepts db connection
│ └── order-repo.ts
└── index.ts ← calls composition root, starts server
For language-specific composition root patterns, see rules/dependency-injection.md.
WITHOUT DI:
Service imports DB directly → can't test without real DB
Service imports config directly → can't test with different config
Must mock imports → fragile, tied to file paths
WITH DI:
Service accepts interfaces → pass fakes in tests
No mocking library needed for most tests
Tests are fast, isolated, don't need infrastructure
shared / instance / module-level vars. Makes testing painful.Container.resolve(Thing) inside business logic hides dependencies.@Autowired lateinit var repo hides what a class needs.import { db } from '../db' hardwires the dependency.Wave 1 (foundation) should set up:
.env.example — every var with description and safe defaulttesting
Multi-source research method — decompose a question, fan out parallel investigators, interleaved-think each result, verify claims adversarially, synthesize a cited answer. Use for breadth-heavy research, stack comparisons, "which approach wins" questions.
testing
Decide when to use unit vs integration vs e2e tests, and when to mock vs use the real thing per dependency. Dependency injection is the enabler — without it you end up monkey-patching imports. Apply when writing tests of any kind.
development
Test-driven development process — write failing test, implement to pass, refactor. Use when implementing any feature or fixing bugs.
development
Patterns for sharing types, API contracts, and validation schemas between frontend and backend. Use when multiple domains consume the same data shapes to prevent contract drift.