Skip to content

Execution-scoped providers with next(instance) for transactional DI (e.g. @InjectTransactionalRepository) #16350

@purerosefallen

Description

@purerosefallen

Is there an existing issue that is already proposing this?

  • I have searched the existing issues

Is your feature request related to a problem? Please describe it

Problem statement

I have an issue when I want provider resolution to participate in the request execution lifecycle, not only in the DI construction lifecycle.

Today, providers are value-oriented (useClass, useFactory, useValue), and execution lifecycle is handled separately (e.g. handler wrappers).
For some use cases, this separation makes dependency injection awkward.

Concrete example (TypeORM):

  • I want to write business services as request-scoped services.
  • I want those services to inject @InjectTransactionalRepository(Entity).
  • I want repository instances to be bound to a transaction started at execution time via entityManager.transaction(...).
  • I do not want each controller/handler to manually add transaction wrappers.

In short: I need a first-class way for providers to be resolved through execution lifecycle boundaries, so injected dependencies can come from execution-time context (transaction, tenant context, etc.).

Important requirement: request-scoped providers and execution-scoped providers must be able to depend on each other (mutual dependency), not only one-way dependency.

Describe the solution you'd like

Proposed solution

Introduce a new provider form that is execution-oriented.

Example API shape:

{
  provide: SOME_TOKEN,
  inject: [...deps],
  async useExecution(
    next: (instance: unknown) => Promise<unknown>,
    ...deps: unknown[]
  ) {
    // build execution-time instance, then pass it to next(instance)
    return next(instance);
  },
}

Semantics:

  • useExecution runs during handler execution lifecycle (before/after handler), not during bootstrap construction.
  • next(instance) means: continue execution and inject this concrete provider instance for the downstream execution chain.
  • framework composes all useExecution providers into a deterministic chain and awaits the final result.
  • request-scoped and execution-scoped providers can depend on each other in the same dependency graph (mutual dependency support), with deterministic resolution order.

TypeORM example with entityManager.transaction:

{
  provide: getRepositoryToken(User),
  inject: [DataSource],
  async useExecution(next, dataSource: DataSource) {
    return dataSource.manager.transaction(async (em) => {
      const transactionalRepo = em.getRepository(User);
      return next(transactionalRepo);
    });
  },
}

Business service:

@Injectable({ scope: Scope.REQUEST })
export class UserService {
  constructor(
    @InjectTransactionalRepository(User)
    private readonly userRepo: Repository<User>,
  ) {}
}

This allows transactional repository injection from execution-time transaction boundaries without manual per-handler wiring.

Expected dependency behavior:

  • A request-scoped business service can depend on @InjectTransactionalRepository(Entity).
  • The transactional repository provider can depend on execution-time transaction context.
  • An execution-scoped provider can also depend on request-scoped dependencies.
  • The framework should resolve this as one ordered graph for a single request execution.

Teachability, documentation, adoption, migration strategy

Teachability, documentation, adoption, migration strategy

How users would use it

  1. Register execution-scoped providers in a module, same place where providers are declared.
  2. Keep normal constructor injection in business services.
  3. Use dedicated decorators like @InjectTransactionalRepository(Entity) to inject the token exposed by an execution-scoped provider.

Documentation outline

  1. New chapter: Execution-scoped providers
  2. API contract: useExecution(next, ...deps) and next(instance)
  3. Execution ordering and composition rules
  4. Mutual dependency rules between scopes:
    • request scope depends on execution scope
    • execution scope depends on request scope
    • deterministic per-request graph order
  5. TypeORM guide:
    • Start transaction with dataSource.manager.transaction(...)
    • Resolve repository with em.getRepository(Entity)
    • Inject with @InjectTransactionalRepository(Entity)
  6. Interop section:
    • Works with existing provider styles
    • Works with existing scopes and module boundaries

Adoption strategy

  • Optional feature: existing apps keep current behavior with no changes.
  • Teams can adopt per module or per domain.
  • Typical first migration target: transaction-heavy services.

Migration example

Before:

  • custom transaction wrappers around service/controller logic
  • manual transaction context plumbing

After:

  • transaction boundary declared once in execution-scoped provider
  • business service keeps regular DI with @InjectTransactionalRepository()
  • request and execution scopes can reference each other in one request execution graph

What is the motivation / use case for changing the behavior?

Motivation / use case

Primary use case: transactional dependency injection with TypeORM.

I want to model transactions as execution boundaries while still using standard constructor injection in request-scoped business services.
I also need request-scoped and execution-scoped providers to depend on each other in the same request flow.

Concrete flow:

  1. Execution starts for a handler.
  2. A TypeORM execution-scoped provider opens entityManager.transaction(...).
  3. Inside that boundary, it resolves and passes em.getRepository(Entity) via next(instance).
  4. Request-scoped services consume @InjectTransactionalRepository(Entity) normally.
  5. The whole chain runs in one transaction boundary.

Scope dependency expectation:

  • request-scoped providers can consume values produced by execution-scoped providers
  • execution-scoped providers can read request-scoped context values
  • execution composition and DI resolution remain separate but coordinated per request

Why this matters:

  • keeps business code focused on domain behavior
  • avoids repeating transaction setup in handlers
  • enables consistent transactional behavior across service layers
  • keeps DI expressive for execution-time resources

Additional use cases beyond TypeORM:

  • tenant-bound repositories/connections selected at execution time
  • execution-bound security/audit resources
  • execution-time contextual resources that should still be injectable

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions