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.

  • Architecture
  • $dependency-injection
  • 201 lines
  • 2 files
  • ~2,000 tokens
  • MIT
  • Updated

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- dependency-injection

Installs to ~/.agents/skills/dependency-injection/ for every project on this machine.

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

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.

~/.agents/skills/dependency-injection/SKILL.md

--- name: dependency-injection description: Applies dependency injection by constructor parameters and a single composition root, and configures containers correctly - .NET… license: MIT ---

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

  1. Read AGENTS.md and existing wiring: Program.cs, main.go, app.module.ts, @Configuration classes, FastAPI dependencies or a container.py. Follow the style already in use; do not introduce a container to a codebase that wires by hand well.
  2. Find where dependencies are created inline. Typical smells: rg -n "new (HttpClient|SqlConnection|.*Repository)\(|getInstance\(\)|ServiceLocator|os\.environ\[" src.
  3. Check how tests replace collaborators today (monkeypatch, jest.mock, reflection). Heavy patching marks the seams DI should create.

Decide how much DI you need

SituationRecommendation
Go, Rust, small Python or TS servicesManual 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 linesgoogle/wire (compile-time codegen) or uber-go/fx if you want lifecycle hooks
Python web appFastAPI Depends for request-scoped values; plain constructors for domain services
AndroidHilt for Jetpack apps; Koin for Kotlin Multiplatform
Library codeNever 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

LifetimeUse forNever
SingletonStateless services, clients that pool connections (HTTP clients, DB pools, Redis), configurationHolding per-request state or a scoped dependency
Scoped (per request or unit of work)DbContext/session, unit of work, current-user contextCapturing in a singleton or a background thread
TransientLightweight, stateless helpersExpensive 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

  1. Pick one class with a hard-wired dependency that blocks a test.
  2. Add a constructor parameter for the dependency; keep a default only temporarily if many callers construct the class by hand.
  3. Move construction to the composition root (or container registration).
  4. Replace patching in tests with a fake passed through the constructor.
  5. 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.mock or reflection is needed for the refactored classes.
  • rg shows no remaining new 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.yamlgenerated from this page

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.

esc
Just launched

Codex Skills Pro is live

79 premium skills for Codex, each with scripts, references and an evidence report that proves the result.

Bought one by one$882.21
All 79 skills$49
94% off

That's $0.62 a skill, paid once.

Get all 79 for $49Or pick single skills from $4.99 →

Secure checkout by Creem