home / skills / psincraian / myfy / dependency-injection

This skill explains and demonstrates myfy dependency injection with scopes, providers, and route integration to improve modularity and testability.

npx playbooks add skill psincraian/myfy --skill dependency-injection

Review the files below or copy the command above to add this skill to your agents.

Files (1)
SKILL.md
5.2 KB
---
name: dependency-injection
description: myfy dependency injection with scopes (SINGLETON, REQUEST, TASK). Use when working with @provider decorator, DI container, scopes, injection patterns, or understanding how WebModule, DataModule, FrontendModule, TasksModule, UserModule, CliModule, AuthModule, and RateLimitModule use dependency injection.
---

# Dependency Injection in myfy

myfy uses constructor-based dependency injection with three scopes.

## Scopes

| Scope | Lifetime | Use Case |
|-------|----------|----------|
| SINGLETON | Application lifetime | Config, pools, services, caches |
| REQUEST | Per HTTP request | Sessions, user context, request logger |
| TASK | Per background task | Task context, job-specific state |

## Provider Declaration

```python
from myfy.core import provider, SINGLETON, REQUEST, TASK

# SINGLETON: Created once, shared across all requests
@provider(scope=SINGLETON)
def database_pool(settings: DatabaseSettings) -> DatabasePool:
    return DatabasePool(settings.database_url)

# REQUEST: Created per HTTP request, auto-cleaned up
@provider(scope=REQUEST)
def db_session(pool: DatabasePool) -> AsyncSession:
    return pool.get_session()

# TASK: Created per background task execution
@provider(scope=TASK)
def task_logger(ctx: TaskContext) -> Logger:
    return Logger(task_id=ctx.task_id)
```

## Provider Options

```python
@provider(
    scope=SINGLETON,              # Lifecycle scope
    qualifier="primary",          # Optional qualifier for multiple providers
    name="my_database",           # Optional name for resolution
    reloadable=("log_level",),    # Settings that can hot-reload
)
def database(settings: Settings) -> Database:
    return Database(settings.db_url)
```

## Scope Dependency Rules

1. **SINGLETON** can depend on: other singletons only
2. **REQUEST** can depend on: singletons and other request-scoped
3. **TASK** can depend on: singletons and other task-scoped

**SINGLETON cannot depend on REQUEST/TASK** - this fails at compile time!

```python
# WRONG - will fail at startup
@provider(scope=SINGLETON)
def bad_service(session: AsyncSession):  # AsyncSession is REQUEST scope
    return MyService(session)

# CORRECT - use factory pattern
@provider(scope=SINGLETON)
def service_factory(pool: DatabasePool) -> ServiceFactory:
    return ServiceFactory(pool)

@provider(scope=REQUEST)
def service(factory: ServiceFactory, session: AsyncSession) -> MyService:
    return factory.create(session)
```

## Injection in Routes

Parameters are auto-classified in order:
1. **Path parameters** - from URL template like `{user_id}`
2. **Query parameters** - annotated with `Query(...)` or primitives with defaults
3. **Body parameter** - Pydantic model or dataclass
4. **DI dependencies** - everything else (resolved from container)

```python
from myfy.web import route, Query
from myfy.data import AsyncSession

@route.post("/users/{user_id}/orders")
async def create_order(
    user_id: int,                    # Path param
    limit: int = Query(default=10),  # Query param
    body: OrderCreate,               # Request body (Pydantic model)
    session: AsyncSession,           # DI (REQUEST scope)
    settings: AppSettings,           # DI (SINGLETON)
) -> dict:
    ...
```

## Qualifiers for Multiple Providers

When you have multiple providers of the same type:

```python
from myfy.core import provider, SINGLETON, Qualifier
from typing import Annotated

@provider(scope=SINGLETON, qualifier="primary")
def primary_db(settings: Settings) -> Database:
    return Database(settings.primary_url)

@provider(scope=SINGLETON, qualifier="replica")
def replica_db(settings: Settings) -> Database:
    return Database(settings.replica_url)

# Inject by qualifier
@route.get("/users")
async def list_users(
    db: Annotated[Database, Qualifier("replica")]
) -> list[dict]:
    return await db.fetch_all()
```

## Common Patterns

### Factory Pattern (REQUEST from SINGLETON)

```python
@provider(scope=SINGLETON)
def email_client(settings: EmailSettings) -> EmailClient:
    return EmailClient(settings.api_key)

@provider(scope=REQUEST)
def email_sender(client: EmailClient, user: User) -> EmailSender:
    return EmailSender(client, from_user=user)
```

### Optional Dependencies

```python
from typing import Optional

@provider(scope=SINGLETON)
def cache_service(redis: Optional[RedisClient] = None) -> CacheService:
    if redis:
        return RedisCacheService(redis)
    return InMemoryCacheService()
```

## Testing

Override providers in tests using the container's override context:

```python
from myfy.core import override

async def test_with_mock_db():
    mock_db = MockDatabase()

    with override(Database, mock_db):
        # Inside this block, Database resolves to mock_db
        result = await my_service.do_something()

    assert result == expected
```

## Best Practices

1. **Keep providers pure** - No side effects in factory functions
2. **Use SINGLETON for shared resources** - Database pools, HTTP clients, caches
3. **Use REQUEST for request-specific state** - DB sessions, user context
4. **Use TASK for background job state** - Task logger, job-specific clients
5. **Validate at compile time** - All scope violations caught at startup
6. **Return type required** - Provider functions must have return type annotation

Overview

This skill documents myfy's constructor-based dependency injection system with three lifecycle scopes: SINGLETON, REQUEST, and TASK. It explains how to declare providers with the @provider decorator, use qualifiers and names, and how modules like WebModule, DataModule, TasksModule, and others commonly rely on DI. The goal is to make dependency patterns explicit, avoid lifecycle mistakes, and speed up correct wiring of services.

How this skill works

Providers are ordinary functions annotated with @provider and a scope; the DI container creates and injects their return values where needed. SINGLETON instances live for the application lifetime, REQUEST instances are created per HTTP request, and TASK instances are created per background task. The container enforces scope rules at startup and supports qualifiers, named providers, reloadable settings, and test-time overrides.

When to use it

  • Wiring shared resources like DB pools, HTTP clients, and caches (SINGLETON).
  • Creating per-request resources such as DB sessions, request logger, or user context (REQUEST).
  • Providing task-scoped items like job context or task-specific loggers (TASK).
  • When using @provider, qualifiers, or name-based resolution for multiple providers.
  • When writing routes that mix path/query/body params and DI-resolved parameters.

Best practices

  • Keep provider functions pure and free of side effects; construct objects only.
  • Prefer SINGLETON for connection pools and expensive shared resources. Use REQUEST for request-local state and TASK for background-job state.
  • Avoid SINGLETON depending on REQUEST or TASK scoped types; use factory providers to bridge scopes.
  • Annotate return types on all providers so the container can resolve types reliably.
  • Use qualifiers for multiple providers of the same type and override() in tests for safe mocks.

Example use cases

  • Declare a SINGLETON database_pool used by DataModule and Services across the app.
  • Create a REQUEST-scoped AsyncSession injected into route handlers for transactional work.
  • Provide a TASK-scoped task_logger for background job tracing in TasksModule.
  • Expose two Database providers (primary, replica) and inject by Qualifier in different routes.
  • Use a singleton factory to produce request-scoped services when a singleton must not capture request state.

FAQ

What happens if a SINGLETON depends on a REQUEST provider?

Scope violations are detected at startup and will fail; use a factory SINGLETON that creates REQUEST-scoped instances instead.

How do I choose between QUALIFIER and name?

Use qualifier annotations when you need type-based selection in code; use names for explicit lookup or tooling scenarios.

Can I hot-reload provider settings?

Yes—declare reloadable setting names on the provider to allow hot-reload of configured fields.