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?
// 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 {}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
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?
// 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() };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?
// 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'; }}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?
// 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$/);}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?
// 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');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?
// 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;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?
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 {}Go deeper
Pro tips
- 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).
- Unit-test the factory function itself. Export the function a
useFactorycalls 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" (sourcecontent/fundamentals/dependency-injection.mdin 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. "TheuseFactorysyntax 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 onuseFactoryreturning 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.
Related lessons
Checked against the tools and docs as of 2026-10-13.