Skip to content

@provides

@injectable covers 99% of dependency wiring. Use @provides when the concrete implementation of an interface must be chosen at runtime — typically based on a feature flag, environment, or config rule.

When to reach for @provides

  • A composite/strategy binding where the selected implementation depends on runtime state (feature flag, region, tenant, environment).
  • A third-party SDK client that needs non-trivial construction and can't be expressed as a single @injectable class.

If you just need to register a class, use @injectable. If you need a manual binding for a value that never changes (e.g. a constant config object), prefer extra_modules.

Basic usage

from abc import ABC, abstractmethod
from django_autowired import injectable, provides


class IPaymentGateway(ABC):
    @abstractmethod
    def charge(self, amount: int) -> str: ...


class StripeV1Gateway(IPaymentGateway):
    def charge(self, amount: int) -> str:
        return f"stripe-v1:{amount}"


class StripeV2Gateway(IPaymentGateway):
    def charge(self, amount: int) -> str:
        return f"stripe-v2:{amount}"


@provides(bind_to=IPaymentGateway)
def payment_gateway(flags: FeatureFlags) -> IPaymentGateway:
    if flags.is_enabled("use_stripe_v2"):
        return StripeV2Gateway()
    return StripeV1Gateway()

FeatureFlags is resolved from the container just like any @injectable dependency — provider functions support full dependency injection.

Signature

@provides(bind_to: type, scope: Scope = Scope.SINGLETON)
  • bind_torequired. The interface/type the factory's return value binds to. A provider without bind_to is rejected at decoration time.
  • scope — Lifecycle scope the container applies to factory invocation.

Scope semantics

@provides is a factory — the framework owns the scope around it:

Scope Factory invoked
SINGLETON (default) Once per container lifetime. Result cached.
TRANSIENT On every container.get() call.
THREAD Once per thread. Result cached per-thread.

This matches Spring @Bean / Dagger @Provides semantics. Your factory does not manage caching — the framework does.

Factory injection

Factory parameters are resolved from the container. All parameters must be type-annotated:

@provides(bind_to=IPaymentGateway)
def payment_gateway(
    flags: FeatureFlags,
    cfg: StripeConfig,
    logger: ILogger,
) -> IPaymentGateway:
    if flags.is_enabled("use_stripe_v2"):
        return StripeV2Gateway(cfg, logger)
    return StripeV1Gateway(cfg, logger)

A factory parameter without an annotation raises TypeError at build time.

One binding per target

Whether you use @injectable or @provides, each interface can be bound exactly once. Any combination of:

  • two @injectable classes with the same bind_to
  • two @provides functions with the same bind_to
  • one @injectable + one @provides with the same bind_to

…raises DuplicateBindingError. This is a feature: predictable, deterministic resolution is a core guarantee of django-autowired.

If you need multiple candidates selected at runtime, express the conditional logic inside a single @provides function that returns the correct instance.

Eager evaluation

Every @provides factory runs once during container.initialize() to catch errors at boot:

  • Factory raises → ProviderResolutionError wraps the exception.
  • Factory has unresolvable dependencies → ProviderResolutionError.

For SINGLETON scope this eager invocation produces the cached instance. For TRANSIENT and THREAD scope the instance is discarded — the eager run is pure verification. This matches the library's "fail loudly at boot" principle.

What @provides cannot do

  • Cannot decorate a class. @provides only decorates functions.
  • Cannot be called without bind_to. Raises TypeError immediately.
  • Cannot chain/fallback. Multiple providers for the same target are an error, not an ordered list.

FAQ

Q: Should my factory return a class or an instance? An instance. @provides is a factory. The framework handles scope/caching.

Q: What if the factory needs to construct something complex with many deps? Declare those deps as factory parameters — they are resolved from the container just like @injectable constructor parameters.

Q: Can a @provides factory depend on another @provides factory? Yes. Dependency resolution across factories is handled by the backend.