Skip to content

Factory, abstract factory and NestJS useFactory

Put the "which class?" decision in one function; build a family from one choice; pick per call only what really changes per call.

Predict first

Before you read: predict

ExamHall's uni-north deployment picks its proctoring vendor from the tenant's contract, which it loads when the app starts. The app is started with ProctoringModule, then open('c-7') is called on ExamSessionService once. The files are below.

What happens?

Code for this question
// proctoring/vendors.tsexport interface ProctoringVendor { start(candidateId: string): string; } export class ProctorlyAdapter implements ProctoringVendor {  start(candidateId: string) { return `proctorly session ${candidateId}`; }} export class WatchDeskAdapter implements ProctoringVendor {  start(candidateId: string) { return `watchdesk session ${candidateId}`; }} // proctoring/proctoring.module.tsimport { Inject, Injectable, Module } from '@nestjs/common';import { ProctorlyAdapter, ProctoringVendor, WatchDeskAdapter } from './vendors.js'; export const PROCTORING_VENDOR = Symbol('PROCTORING_VENDOR'); @Injectable()export class DeploymentConfig { tenant() { return 'uni-north'; } } @Injectable()export class TenantContracts {  async vendorFor(tenant: string) { return tenant === 'uni-north' ? 'watchdesk' : 'proctorly'; }} @Injectable()export class ExamSessionService {  constructor(@Inject(PROCTORING_VENDOR) private vendor: ProctoringVendor) {}  open(candidateId: string) { return this.vendor.start(candidateId); }} @Module({  providers: [    DeploymentConfig,    TenantContracts,    ExamSessionService,    {      provide: PROCTORING_VENDOR,      inject: [DeploymentConfig, TenantContracts],      useFactory: async (config: DeploymentConfig, contracts: TenantContracts) =>        (await contracts.vendorFor(config.tenant())) === 'watchdesk' ? new WatchDeskAdapter() : new ProctorlyAdapter(),    },  ],})export class ProctoringModule {}
Pick one

The 30-second version

A factory owns one decision: which concrete class to build. Callers ask for an interface. A plain function is enough, and Nest's useFactory is one: Nest awaits an async factory before building anything that injects it. An abstract factory makes one choice and builds a matching family. A provider's factory runs once; a per-call choice runs per call.

Put the "which class?" decision in one function; build a family from one choice; pick per call only what really changes per call.

How it works

Step 1 of 12

The factory owns the choice. Callers ask for an interface; one function picks the class, replacing scattered new with environment checks (pat-code-smells-to-patterns step 13).
In the kitchen
  • the customer asks for "a kitchen", never a maker's part number; the designer picks the supplier range from the budget → callers ask for the interface; the factory owns the choice (step 1)

Analogy: A fitted-kitchen showroom (home improvement)

In the showroom In code
the customer asks for "a kitchen", never a maker's part number; the designer picks the supplier range from the budget callers ask for the interface; the factory owns the choice (step 1)
carcasses, doors and hinges all come from the one range she picked, because one range's doors do not hang on another's carcasses an abstract factory: one choice, one matching family (step 10)
doors ordered by one designer and carcasses by another, each reading the brief their own way separate choices drift (step 11)
she draws nothing until the surveyor's measurements arrive an async factory is awaited (step 5)

In real terms: the brief names what is wanted; one person decides which range supplies all of it.

Breaks down when: a designer can swap a door after delivery; a provider's choice is fixed once Nest has built it (step 7).

Contrasting case: in Java with Spring, a @Bean method is the same idea: a method returning the interface, run by the container, often reading configuration to pick the class.

Examples, run for real

4.1 ExamHall EXH-4040: the vendor named inside the service

ExamSessionService built its own adapter, private vendor = new ProctorlyAdapter(). When uni-north signed with WatchDesk, its sessions still went to Proctorly, because the choice lived in the service. MR !4340 moves it into one PROCTORING_VENDOR provider whose useFactory reads the tenant's contract; the service only injects the token.

4.1 EXH-4040 as shipped (service calls new ProctorlyAdapter()): uni-north (contract watchdesk) opens 'proctorly session c-7'
4.1 MR !4340 PROCTORING_VENDOR useFactory reads the contract: uni-north opens 'watchdesk session c-7' · city-college (contract proctorly) opens 'proctorly session c-7'
4.1 MR !4340 ExamSessionService source: 0 `new` expressions · 0 imports of an adapter class

4.2 One contract read, one family

WatchDesk also sends results back by webhook, and only WatchDesk's verifier accepts WatchDesk's signatures. The MR's PROCTORING_KIT factory reads the contract once and returns both parts from that one answer (step 10).

4.2 PROCTORING_KIT for uni-north: sessions WatchDeskSessions · webhooks WatchDeskWebhooks · contract read 1 time

Try it

Answer first, then check. Nothing is saved.

Question 1 of 6

A streaming app plays each title with a player and a licence client. Both must use the DRM the device needs: FairPlay on safari and ios-app, Widevine on chrome.

The team tries three versions of playback.ts:

// playback.ts (version 1)
import { FairPlayLicence, FairPlayPlayer, WidevineLicence, WidevinePlayer } from './drm.js';

const playerFor = (device: string) => (device === 'safari' ? new FairPlayPlayer() : new WidevinePlayer());
const licenceFor = (device: string) => (['safari', 'ios-app'].includes(device) ? new FairPlayLicence() : new WidevineLicence());
export const kitFor = (device: string) => ({ player: playerFor(device), licence: licenceFor(device) });
// playback.ts (version 2)
import { DrmKit, fairplayKit, widevineKit } from './drm.js';

const kitOf = (device: string): DrmKit => (['safari', 'ios-app'].includes(device) ? fairplayKit : widevineKit);
export const kitFor = (device: string) => {
  const kit = kitOf(device);
  return { player: kit.player(), licence: kit.licence() };
};
// playback.ts (version 3)
import { DrmKit, fairplayKit, widevineKit } from './drm.js';

let kit: DrmKit | undefined;
const kitOf = (device: string): DrmKit => (kit ??= ['safari', 'ios-app'].includes(device) ? fairplayKit : widevineKit);
export const kitFor = (device: string) => ({ player: kitOf(device).player(), licence: kitOf(device).licence() });

The app calls kitFor for chrome, then safari, then ios-app.

Which version gives every device a player and a licence client of the DRM it needs?

Code for this question
// drm.tsexport interface Player { drm: string; }export interface LicenceClient { drm: string; }export interface DrmKit { player(): Player; licence(): LicenceClient; } export class WidevinePlayer implements Player { drm = 'widevine'; }export class FairPlayPlayer implements Player { drm = 'fairplay'; }export class WidevineLicence implements LicenceClient { drm = 'widevine'; }export class FairPlayLicence implements LicenceClient { drm = 'fairplay'; } export const widevineKit: DrmKit = { player: () => new WidevinePlayer(), licence: () => new WidevineLicence() };export const fairplayKit: DrmKit = { player: () => new FairPlayPlayer(), licence: () => new FairPlayLicence() };
Pick one

Question 2 of 6

A streaming platform signs each play's URL with the CDN in the studio's contract. Every studio used to have its own deployment; now one deployment serves them all, so the signer is picked on every play.

The team tries three versions of playback.module.ts:

// playback.module.ts (version 1)
import { Injectable, Module } from '@nestjs/common';
import { signerFor, StudioContracts } from './signing.js';

@Injectable()
export class PlaybackService {
  constructor(private contracts: StudioContracts) {}
  play(studio: string, titleId: string) { return signerFor(this.contracts.cdnFor(studio)).sign(titleId); }
}

@Module({ providers: [StudioContracts, PlaybackService] })
export class AppModule {}
// playback.module.ts (version 2)
import { Inject, Injectable, Module } from '@nestjs/common';
import { NorthSigner, SkySigner, StudioContracts, UrlSigner } from './signing.js';

export const SIGNERS = Symbol('SIGNERS');

@Injectable()
export class PlaybackService {
  constructor(@Inject(SIGNERS) private signers: Record<string, UrlSigner>, private contracts: StudioContracts) {}
  play(studio: string, titleId: string) { return this.signers[this.contracts.cdnFor(studio)].sign(titleId); }
}

@Module({
  providers: [StudioContracts, PlaybackService, {
    provide: SIGNERS,
    useFactory: (): Record<string, UrlSigner> => ({ north: new NorthSigner(), sky: new SkySigner() }),
  }],
})
export class AppModule {}
// playback.module.ts (version 3)
import { Inject, Injectable, Module } from '@nestjs/common';
import { NorthSigner, SkySigner, StudioContracts, UrlSigner } from './signing.js';

export const SIGNERS = Symbol('SIGNERS');

@Injectable()
export class PlaybackService {
  constructor(@Inject(SIGNERS) private signers: Record<string, UrlSigner>, private contracts: StudioContracts) {}
  play(studio: string, titleId: string) { return this.signers[this.contracts.cdnFor(studio)].sign(titleId); }
}

@Module({
  providers: [StudioContracts, PlaybackService, {
    provide: SIGNERS,
    useFactory: (): Record<string, UrlSigner> => ({
      get north() { return new NorthSigner(); },
      get sky() { return new SkySigner(); },
    }),
  }],
})
export class AppModule {}

The app starts, then plays t-1 for studio-a, t-2 for studio-b and t-3 for studio-a.

Which version signs every play with its studio's CDN and builds each signer once?

Code for this question
// signing.tsimport { Injectable } from '@nestjs/common'; export interface UrlSigner { sign(titleId: string): string; }export class NorthSigner implements UrlSigner { sign(titleId: string) { return `north/${titleId}`; } }export class SkySigner implements UrlSigner { sign(titleId: string) { return `sky/${titleId}`; } } export const signerFor = (cdn: string): UrlSigner => (cdn === 'sky' ? new SkySigner() : new NorthSigner()); @Injectable()export class StudioContracts {  cdnFor(studio: string) { return studio === 'studio-b' ? 'sky' : 'north'; }}
Pick one

Question 3 of 6

A streaming app picks its URL signer in one factory function. A reviewer wants one spec that passes as the code stands and fails if someone swaps the two branches of signerFor. The files are below.

Which spec does that?

Code for this question
// signing.tsexport interface UrlSigner { sign(titleId: string): string; }export class NorthSigner implements UrlSigner { sign(titleId: string) { return `north/${titleId}`; } }export class SkySigner implements UrlSigner { sign(titleId: string) { return `sky/${titleId}`; } } export function signerFor(cdn: string): UrlSigner {  return cdn === 'sky' ? new SkySigner() : new NorthSigner();} // playback.module.tsimport { Inject, Injectable, Module } from '@nestjs/common';import { signerFor, UrlSigner } from './signing.js'; export const SIGNER = Symbol('SIGNER'); @Injectable()export class CdnConfig { cdn() { return 'sky'; } } @Injectable()export class PlaybackService {  constructor(@Inject(SIGNER) private signer: UrlSigner) {}  play(titleId: string) { return this.signer.sign(titleId); }} @Module({  providers: [CdnConfig, PlaybackService, { provide: SIGNER, inject: [CdnConfig], useFactory: (config: CdnConfig) => signerFor(config.cdn()) }],})export class PlaybackModule {} // playback.a.spec.tsimport assert from 'node:assert/strict';import { Test } from '@nestjs/testing';import { PlaybackModule, PlaybackService, SIGNER } from './playback.module.js'; export async function run() {  const moduleRef = await Test.createTestingModule({ imports: [PlaybackModule] })    .overrideProvider(SIGNER)    .useValue({ sign: (titleId: string) => `sky/${titleId}` })    .compile();  assert.equal(moduleRef.get(PlaybackService).play('t-1'), 'sky/t-1');} // signing.b.spec.tsimport assert from 'node:assert/strict';import { SkySigner, signerFor } from './signing.js'; export async function run() {  assert.ok(signerFor('sky') instanceof SkySigner);} // playback.c.spec.tsimport assert from 'node:assert/strict';import { Test } from '@nestjs/testing';import { PlaybackModule, PlaybackService } from './playback.module.js'; export async function run() {  const moduleRef = await Test.createTestingModule({ imports: [PlaybackModule] }).compile();  assert.match(moduleRef.get(PlaybackService).play('t-1'), /^\w+\/t-1$/);}
Pick one

Question 4 of 6

A streaming app's takedown job uses the URL-signer factory. The team runs tsc (strict) over its two files, which are below.

What does it report?

Code for this question
// signing.tsexport interface UrlSigner { sign(titleId: string): string; } export class NorthSigner implements UrlSigner {  sign(titleId: string) { return `north/${titleId}`; }  purge(titleId: string) { return `purged ${titleId}`; }} export class SkySigner implements UrlSigner {  sign(titleId: string) { return `sky/${titleId}`; }} export function signerFor(cdn: string): UrlSigner {  return cdn === 'north' ? new NorthSigner() : new SkySigner();} // takedown.tsimport { signerFor } from './signing.js'; const signer = signerFor('north');export const purged = signer.purge('t-9');
Pick one

Question 5 of 6

A box office sets up each venue's gates: the ticket printer and the gate scanner must use one format. The scanner team cleans each venue's contract value before its factory reads it; the printer team passes the value as it is. The files are below.

How many venues get a printer and a scanner that agree: what is agree?

Code for this question
// gates.tsexport interface TicketPrinter { format: string; }export interface GateScanner { reads: string; } export class QrPrinter implements TicketPrinter { format = 'qr'; }export class NfcPrinter implements TicketPrinter { format = 'nfc'; }export class QrScanner implements GateScanner { reads = 'qr'; }export class NfcScanner implements GateScanner { reads = 'nfc'; } export const normalise = (gates: string) => gates.trim().toLowerCase();export const printerFor = (gates: string): TicketPrinter => (gates === 'nfc' ? new NfcPrinter() : new QrPrinter());export const scannerFor = (gates: string): GateScanner => (gates === 'nfc' ? new NfcScanner() : new QrScanner()); // setup.tsimport { normalise, printerFor, scannerFor } from './gates.js'; const contracts: Record<string, string> = { arena: 'nfc', theatre: 'QR', stadium: 'NFC', expo: 'nfc ' };export const agree = Object.values(contracts).filter((gates) => {  const scanner = scannerFor(normalise(gates));  const printer = printerFor(gates);  return printer.format === scanner.reads;}).length;
Pick one

Question 6 of 6

A ledger picks its exchange-rate source from its contract when the app starts. The contract store is down. The app starts with AppModule, then calls settle(1000) once. The code is below.

What happens?

Code for this question
import { Inject, Injectable, Module } from '@nestjs/common'; export interface FxSource { rate(currency: string): number; }export class EcbRates implements FxSource { rate(currency: string) { return currency === 'EUR' ? 0.92 : 1; } }export class OandaRates implements FxSource { rate(currency: string) { return currency === 'EUR' ? 0.93 : 1; } } export class ContractStoreDown extends Error {} @Injectable()export class LedgerContracts {  async fxProvider(): Promise<string> { throw new ContractStoreDown('contract store unreachable'); }} export const FX_SOURCE = 'FX_SOURCE'; @Injectable()export class SettlementService {  constructor(@Inject(FX_SOURCE) private fx: FxSource) {}  settle(cents: number) { return Math.round(cents * this.fx.rate('EUR')); }} @Module({  providers: [LedgerContracts, SettlementService, {    provide: FX_SOURCE,    inject: [LedgerContracts],    useFactory: async (contracts: LedgerContracts) => ((await contracts.fxProvider()) === 'oanda' ? new OandaRates() : new EcbRates()),  }],})export class AppModule {}
Pick one

Go deeper

Pro tips
  1. Keep the branch in one provider. If two services read the same setting to choose a class, move the choice into one factory provider and inject the token. When: a vendor or region switch appears in more than one file (step 1).
  2. Unit-test the factory function itself. Export the function a useFactory calls and test it with fixture inputs; an override in a module test never runs it. When: the choice has more than one branch (step 12). Verify (RAN, tips.mjs): a spec checking the factory for both CDNs failed 0 of 2 checks as written and 2 of 2 once the two branches were swapped.
Source
  • NestJS documentation, Custom providers, section "Factory providers: useFactory" (source content/fundamentals/dependency-injection.md in the docs site's repository): https://docs.nestjs.com/fundamentals/custom-providers · fetched 2026-10-13 from https://raw.githubusercontent.com/nestjs/docs.nestjs.com/master/content/fundamentals/dependency-injection.md. The curriculum row's second source. "The useFactory syntax lets you create providers dynamically. The provider's value is whatever the factory function returns" (steps 2, 4). Valid as of 2026-10-13.
  • NestJS documentation, Asynchronous providers (source content/fundamentals/async-components.md): https://docs.nestjs.com/fundamentals/async-providers · fetched 2026-10-13. Not named by the row; it is the same docs set's page on useFactory returning a promise. Teaching only; every item is executed. "Nest awaits resolution of the promise before instantiating any class that depends on (injects) such a provider" (step 5). Valid as of 2026-10-13.
  • Gamma, Helm, Johnson and Vlissides, Design Patterns (1994), Factory Method and Abstract Factory: the row's first source, a book; it keys nothing. Steps 10–11 rest on P1 and D1's runs.

Checked against the tools and docs as of 2026-10-13.