Dependency Injection skill for Codex
Constructor wiring that makes tests trivial. Codex moves hard-wired clients and repositories into constructor parameters, builds them in one composition root, and picks singleton, scoped or transient lifetimes on purpose so a scoped DbContext never ends up captured by a singleton. It turns on container validation, prefers plain wiring in Go and small services, and replaces monkeypatching in tests with fakes passed through constructors.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- dependency-injection Installs to ~/.agents/skills/dependency-injection/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install dependency-injection The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project dependency-injection Installs to .agents/skills/dependency-injection/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/dependency-injection.zipunzip -o dependency-injection.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) dependency-injection PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $dependency-injection, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Applies dependency injection by constructor parameters and a single composition root, and configures containers correctly - .NET Microsoft.Extensions.DependencyInjection lifetimes, Spring constructor injection, NestJS providers, FastAPI Depends, Go manual wiring or Wire, Hilt and Koin. Use when code news up its own database clients, tests need monkeypatching to swap dependencies, a container throws a scope or circular dependency error, or someone asks whether to add a DI framework.
Use it when
- Business code constructs its own clients, connections or clocks
- Tests rely on monkeypatching or module mocking to swap collaborators
- A container reports scope, lifetime or circular dependency errors
- You are deciding whether a project needs a DI framework
Use something else when
- You are restructuring layers and ports around the domain: use hexagonal-architecture
- The question is NestJS module structure beyond providers: use nestjs-modules
How to check the result
The application starts with container validation enabled, and the refactored classes are unit tested by passing fakes to constructors with no patching or reflection.
The mistake it prevents
Injecting a scoped service such as a DbContext into a singleton, which silently shares one request's state with every later request.
Done well means objects receive what they need through their constructor, one place in the program (the composition root) decides which concrete implementations to build, each registration has a deliberate lifetime, and tests swap collaborators by passing fakes, not by patching globals.
Before changing anything
- Read
AGENTS.mdand existing wiring:Program.cs,main.go,app.module.ts,@Configurationclasses, FastAPIdependenciesor acontainer.py. Follow the style already in use; do not introduce a container to a codebase that wires by hand well. - Find where dependencies are created inline. Typical smells:
rg -n "new (HttpClient|SqlConnection|.*Repository)\(|getInstance\(\)|ServiceLocator|os\.environ\[" src. - Check how tests replace collaborators today (monkeypatch,
jest.mock, reflection). Heavy patching marks the seams DI should create.
Decide how much DI you need
| Situation | Recommendation |
|---|---|
| Go, Rust, small Python or TS services | Manual constructor wiring in main; no container |
| Framework with a built-in container (ASP.NET Core, Spring, NestJS, Angular) | Use the framework container; do not add a second one |
Large Go codebase where main wiring is hundreds of lines | google/wire (compile-time codegen) or uber-go/fx if you want lifecycle hooks |
| Python web app | FastAPI Depends for request-scoped values; plain constructors for domain services |
| Android | Hilt for Jetpack apps; Koin for Kotlin Multiplatform |
| Library code | Never require a container; accept dependencies as parameters and offer defaults |
Default: constructor injection. Property and setter injection only for optional dependencies or framework hooks that require them. Never inject a container or service locator into business code; it hides dependencies and defeats the compiler.
Lifetimes
| Lifetime | Use for | Never |
|---|---|---|
| Singleton | Stateless services, clients that pool connections (HTTP clients, DB pools, Redis), configuration | Holding per-request state or a scoped dependency |
| Scoped (per request or unit of work) | DbContext/session, unit of work, current-user context | Capturing in a singleton or a background thread |
| Transient | Lightweight, stateless helpers | Expensive objects or anything holding sockets |
The captive dependency bug: a singleton that takes a scoped dependency keeps the first request’s instance forever (stale DbContext, wrong tenant, cross-request data leak). The rule is that a service may only depend on services with equal or longer lifetimes.
Language recipes
.NET
builder.Services.AddSingleton(TimeProvider.System);
builder.Services.AddScoped<IOrderRepository, SqlOrderRepository>();
builder.Services.AddScoped<OrderService>();
builder.Services.AddHttpClient<IPaymentsClient, PaymentsClient>(); // typed client, pooled handlers
builder.Host.UseDefaultServiceProvider(o =>
{
o.ValidateScopes = true; // throws on captive dependencies
o.ValidateOnBuild = true; // throws at startup for missing registrations
});
Inside a BackgroundService (a singleton), create a scope per unit of work with
IServiceScopeFactory.CreateAsyncScope(); never inject a scoped service directly.
Use keyed services (AddKeyedSingleton, [FromKeyedServices("eu")], .NET 8+) instead of
hand-rolled factories when several implementations share an interface.
Spring
@Service
public class OrderService {
private final OrderRepository repo;
private final Clock clock;
public OrderService(OrderRepository repo, Clock clock) { // single constructor: no @Autowired needed
this.repo = repo;
this.clock = clock;
}
}
@Configuration
class TimeConfig {
@Bean Clock clock() { return Clock.systemUTC(); }
}
Field injection (@Autowired on a field) prevents final, hides dependencies and forces
reflection in tests; convert to constructors. Spring Boot forbids circular references by
default since 2.6; fix the cycle rather than setting spring.main.allow-circular-references.
NestJS
@Injectable()
export class OrderService {
constructor(
private readonly repo: OrderRepository,
@Inject(CLOCK) private readonly clock: Clock,
) {}
}
@Module({
providers: [OrderService, OrderRepository, { provide: CLOCK, useValue: systemClock }],
exports: [OrderService],
})
export class OrdersModule {}
Interfaces vanish at runtime in TypeScript, so use a Symbol or string token with
@Inject for interface-typed dependencies. Request-scoped providers make every
dependent request-scoped too; use them sparingly.
Python (FastAPI)
def get_session() -> Iterator[Session]:
with SessionLocal() as session:
yield session
def get_order_service(session: Annotated[Session, Depends(get_session)]) -> OrderService:
return OrderService(OrderRepository(session), clock=SystemClock())
@app.post("/orders")
def create_order(body: OrderIn, svc: Annotated[OrderService, Depends(get_order_service)]):
return svc.create(body)
# test
app.dependency_overrides[get_order_service] = lambda: OrderService(FakeRepo(), FixedClock())
Keep domain classes free of Depends; it belongs only in the web layer.
Go
func main() {
cfg := config.MustLoad()
db := postgres.MustOpen(cfg.DatabaseURL)
repo := orders.NewPostgresRepo(db)
svc := orders.NewService(repo, time.Now)
srv := httpapi.NewServer(svc, cfg.Addr)
log.Fatal(srv.ListenAndServe())
}
Accept small interfaces defined by the consumer (“accept interfaces, return structs”).
Pass functions such as func() time.Time when a whole interface is overkill.
Refactoring code toward DI
- Pick one class with a hard-wired dependency that blocks a test.
- Add a constructor parameter for the dependency; keep a default only temporarily if many callers construct the class by hand.
- Move construction to the composition root (or container registration).
- Replace patching in tests with a fake passed through the constructor.
- Remove the default once callers are migrated. Repeat for the next class.
Do not convert the whole codebase at once; each step should leave tests green.
Gotchas
- Constructor with eight or more parameters is a design smell (the class does too much), not a reason to switch to property injection or a parameter object full of services.
- Interfaces for every class add noise. Introduce one when there is a second implementation, a test fake, or a module boundary to protect.
- Resolving from the container inside business code (
provider.GetService<T>(),applicationContext.getBean) is the service locator pattern; reserve it for the composition root and framework glue. - Disposable singletons created by you (
AddSingleton(new Foo())) are not disposed by the .NET container; register with a factory or type so the container owns them. - Static clocks, random generators and environment reads are hidden dependencies; inject
TimeProvider,Clock, or a function.
Verification
- The app starts with container validation on (.NET
ValidateOnBuild/ValidateScopes, Spring context load test, Nest module compile in an e2e test). - Unit tests construct the class directly with fakes; no
monkeypatch,jest.mockor reflection is needed for the refactored classes. rgshows no remainingnew ConcreteDependency(in the refactored business code.- Run the full test suite from
AGENTS.md; a lifetime mistake often shows up only in integration tests that issue two requests.
Output template
## DI changes: <scope>
**Composition root:** <file>
| Service | Lifetime | Depends on | Notes |
|---|---|---|---|
| OrderService | scoped | IOrderRepository (scoped), TimeProvider (singleton) | was newing SqlConnection |
**Tests updated:** <files, patches removed>
**Validation:** <startup validation result, test command and result> What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "Dependency Injection"
short_description: "Constructor wiring that makes tests trivial"
default_prompt: "$dependency-injection Our services new up their own database clients, refactor this so I can test them" About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.