Loading content...
You've learned to compare patterns, navigate decision trees, and combine patterns effectively. But how do these concepts manifest in real production systems? How do engineers at scale companies apply creational patterns to solve genuine challenges?
This page bridges the gap between academic understanding and practical application. We'll examine case studies drawn from real codebases—database systems, game engines, web frameworks, and enterprise applications. You'll see how the patterns you've studied aren't abstract exercises but are actively shaping the software you use every day.
Each case study follows a consistent structure: the problem, why that pattern was chosen, the implementation, and lessons learned. This gives you templates for applying similar reasoning in your own work.
By the end of this page, you will have seen creational patterns in production contexts across multiple domains. You'll understand how pattern choices are influenced by real constraints, how theoretical patterns adapt to practical needs, and how to reason about patterns when facing similar problems yourself.
Domain: Database client libraries, ORMs, connection managers
Pattern: Object Pool
The problem:
Database connections are expensive resources. Each connection requires:
Creating a new connection for every query would be prohibitively slow. But keeping unlimited connections open would exhaust database server resources.
Why Object Pool:
The Object Pool pattern is the canonical solution. It manages a bounded set of pre-established connections that are borrowed, used, and returned.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145
// Simplified model of production connection pool (e.g., pg-pool, HikariCP) interface Connection { query(sql: string, params?: any[]): Promise<QueryResult>; isAlive(): boolean; reset(): Promise<void>; close(): Promise<void>;} interface PoolConfig { host: string; port: number; database: string; user: string; password: string; minConnections: number; // Keep this many warm maxConnections: number; // Never exceed this idleTimeoutMs: number; // Close idle connections after this acquireTimeoutMs: number; // Max wait time for connection} class ConnectionPool { private available: Connection[] = []; private inUse: Set<Connection> = new Set(); private waitQueue: Array<{ resolve: (conn: Connection) => void; reject: (err: Error) => void; timeout: NodeJS.Timeout; }> = []; constructor(private config: PoolConfig) { // Pre-warm minimum connections this.warmUp(); // Start idle connection reaper this.startReaper(); } private async warmUp(): Promise<void> { for (let i = 0; i < this.config.minConnections; i++) { const conn = await this.createConnection(); this.available.push(conn); } } private async createConnection(): Promise<Connection> { // Expensive operation - TCP connect, auth, etc. return connectToDatabase({ host: this.config.host, port: this.config.port, database: this.config.database, user: this.config.user, password: this.config.password, }); } async acquire(): Promise<Connection> { // Try to get available connection while (this.available.length > 0) { const conn = this.available.pop()!; if (conn.isAlive()) { this.inUse.add(conn); return conn; } // Dead connection - discard and continue await conn.close().catch(() => {}); } // No available connections - can we create new? const totalConnections = this.available.length + this.inUse.size; if (totalConnections < this.config.maxConnections) { const conn = await this.createConnection(); this.inUse.add(conn); return conn; } // At max - wait for return return new Promise((resolve, reject) => { const timeout = setTimeout(() => { const index = this.waitQueue.findIndex(w => w.resolve === resolve); if (index !== -1) { this.waitQueue.splice(index, 1); } reject(new Error('Connection acquire timeout')); }, this.config.acquireTimeoutMs); this.waitQueue.push({ resolve, reject, timeout }); }); } async release(conn: Connection): Promise<void> { if (!this.inUse.has(conn)) { throw new Error('Connection not owned by this pool'); } this.inUse.delete(conn); // Reset connection state before returning to pool await conn.reset(); // If someone is waiting, give to them if (this.waitQueue.length > 0) { const waiter = this.waitQueue.shift()!; clearTimeout(waiter.timeout); this.inUse.add(conn); waiter.resolve(conn); return; } // Otherwise return to available this.available.push(conn); } // Helper for automatic release async withConnection<T>(fn: (conn: Connection) => Promise<T>): Promise<T> { const conn = await this.acquire(); try { return await fn(conn); } finally { await this.release(conn); } } private startReaper(): void { setInterval(() => { const now = Date.now(); // Close idle connections beyond minimum while ( this.available.length > this.config.minConnections && /* connection is idle too long */ true ) { const conn = this.available.shift()!; conn.close().catch(() => {}); } }, 30000); }} // Usage - clean abstractionconst pool = new ConnectionPool(config); // Auto-release patternconst users = await pool.withConnection(async (conn) => { return conn.query('SELECT * FROM users WHERE active = $1', [true]);});Domain: Game development, simulation engines
Patterns: Prototype + Factory (combined)
The problem:
Modern games spawn thousands of entities (enemies, projectiles, particles) per second. Each entity requires:
Creating each from scratch is too slow for real-time performance (16.67ms frame budget at 60fps).
Why Prototype + Factory:
Games use Prototype for the heavy lifting—pre-load archetypes with all expensive data, then clone to spawn. Factory provides the clean API and handles registration/lookup.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137
// Game engine entity spawning system - Prototype + Factory interface EntityPrototype { clone(): Entity; warmUp(): Promise<void>; // Pre-load resources} class Entity { mesh: Mesh; material: Material; animations: Map<string, Animation>; components: Component[]; constructor( mesh: Mesh, material: Material, animations: Map<string, Animation>, components: Component[] ) { this.mesh = mesh; this.material = material; this.animations = new Map(animations); // Shallow copy OK - immutable this.components = components.map(c => c.clone()); // Deep copy - mutable state } setPosition(pos: Vector3): void { /* ... */ } setRotation(rot: Quaternion): void { /* ... */ }} // Pre-configured prototype - resources loaded onceclass GoblinPrototype implements EntityPrototype { private mesh: Mesh | null = null; private material: Material | null = null; private animations: Map<string, Animation> = new Map(); private baseComponents: Component[] = []; async warmUp(): Promise<void> { // Load heavy resources ONCE this.mesh = await loadMesh('assets/goblin/goblin.obj'); this.material = await loadMaterial('assets/goblin/goblin.mat'); const animBundle = await loadAnimations('assets/goblin/animations.bundle'); for (const anim of animBundle) { this.animations.set(anim.name, anim); } // Configure base components this.baseComponents = [ new HealthComponent(100), new AIComponent(new GoblinBehaviorTree()), new CombatComponent({ attackDamage: 15, attackSpeed: 1.2 }), ]; } clone(): Entity { if (!this.mesh) throw new Error('Prototype not warmed up'); // Fast! No loading, just copy/reference return new Entity( this.mesh, // Shared (immutable) this.material, // Shared (immutable) this.animations, // Shallow copy (immutable) this.baseComponents // Deep copied in Entity constructor ); }} // Factory provides clean API and manages prototypesclass EntityFactory { private prototypes: Map<string, EntityPrototype> = new Map(); private loading: Map<string, Promise<void>> = new Map(); async registerPrototype(type: string, prototype: EntityPrototype): Promise<void> { if (this.loading.has(type)) { await this.loading.get(type); return; } const warmUpPromise = prototype.warmUp(); this.loading.set(type, warmUpPromise); await warmUpPromise; this.prototypes.set(type, prototype); this.loading.delete(type); } spawn(type: string, position: Vector3, rotation?: Quaternion): Entity { const prototype = this.prototypes.get(type); if (!prototype) { throw new Error(`Unknown entity type: ${type}. Did you call registerPrototype?`); } const entity = prototype.clone(); entity.setPosition(position); if (rotation) { entity.setRotation(rotation); } return entity; } // Batch spawn for particle systems, etc. spawnMany(type: string, count: number, positionFn: (i: number) => Vector3): Entity[] { const prototype = this.prototypes.get(type); if (!prototype) throw new Error(`Unknown: ${type}`); const entities: Entity[] = []; for (let i = 0; i < count; i++) { const entity = prototype.clone(); entity.setPosition(positionFn(i)); entities.push(entity); } return entities; }} // Usage in game loopasync function initializeGame() { const entityFactory = new EntityFactory(); // Register prototypes at load time await Promise.all([ entityFactory.registerPrototype('goblin', new GoblinPrototype()), entityFactory.registerPrototype('orc', new OrcPrototype()), entityFactory.registerPrototype('arrow', new ProjectilePrototype('arrow')), entityFactory.registerPrototype('fireball', new ProjectilePrototype('fireball')), ]); return entityFactory;} // In gameplay - spawning is now FASTfunction onEnemyWaveTriggered(factory: EntityFactory, waveConfig: WaveConfig) { for (const spawn of waveConfig.spawns) { const entity = factory.spawn(spawn.type, spawn.position); world.addEntity(entity); }}Notice the distinction between immutable resources (mesh, material) that are shared via reference, and mutable components (health, AI state) that must be deep-cloned. Getting this right is crucial for correctness and performance.
Domain: React, Angular, Vue, and other UI frameworks
Pattern: Factory Method (conceptually), with framework-specific adaptations
The problem:
UI frameworks need to:
Why Factory Method (conceptual):
Though modern UI frameworks use different terminology, they embody Factory Method's essence: a method (render, createElement) returns instances without specifying concrete classes. The framework provides the creation hook; your components define what's created.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123
// React's createElement - Factory Method in disguise // The conceptual interfaceinterface ReactElement<P = any> { type: string | ComponentType<P>; props: P; key: string | null;} interface ComponentType<P = {}> { (props: P): ReactElement | null; // Function component} // React.createElement - a factory methodfunction createElement<P>( type: string | ComponentType<P>, props: P | null, ...children: ReactNode[]): ReactElement<P> { return { type, props: { ...props, children: children.length > 0 ? children : undefined, } as P, key: props?.key ?? null, };} // Higher-Order Component - Factory that creates enhanced factoriesfunction withLogger<P extends object>( WrappedComponent: ComponentType<P>): ComponentType<P> { return function LoggedComponent(props: P): ReactElement { console.log(`Rendering ${WrappedComponent.name}`, props); // Factory method - returns element, doesn't know implementation return createElement(WrappedComponent, props); };} // Custom component - defines WHAT is createdfunction Button({ label, onClick }: ButtonProps): ReactElement { return createElement('button', { onClick }, label);} // Usage - factory method pattern in actionconst element = createElement(Button, { label: 'Click me', onClick: handleClick }); // Framework renders - creates actual DOM elements from ReactElement descriptions // === More explicit Abstract Factory in UI frameworks === // Theme-based component factory (Abstract Factory pattern)interface ThemeFactory { createButton(props: ButtonProps): ReactElement; createInput(props: InputProps): ReactElement; createCard(props: CardProps): ReactElement;} class MaterialThemeFactory implements ThemeFactory { createButton(props: ButtonProps): ReactElement { return createElement(MaterialButton, { ...props, ripple: true, elevation: 2, }); } createInput(props: InputProps): ReactElement { return createElement(MaterialInput, { ...props, variant: 'outlined', floatingLabel: true, }); } createCard(props: CardProps): ReactElement { return createElement(MaterialCard, { ...props, elevation: 4, }); }} class MinimalThemeFactory implements ThemeFactory { createButton(props: ButtonProps): ReactElement { return createElement(MinimalButton, { ...props, borderRadius: 0, }); } createInput(props: InputProps): ReactElement { return createElement(MinimalInput, { ...props, underlineOnly: true, }); } createCard(props: CardProps): ReactElement { return createElement(MinimalCard, { ...props, border: '1px solid', }); }} // Theming via React Context - inject Abstract Factoryconst ThemeFactoryContext = React.createContext<ThemeFactory>(new MaterialThemeFactory()); function Form() { const themeFactory = useContext(ThemeFactoryContext); return ( <form> {themeFactory.createInput({ label: 'Email', type: 'email' })} {themeFactory.createInput({ label: 'Password', type: 'password' })} {themeFactory.createButton({ label: 'Sign In', type: 'submit' })} </form> );}Domain: Logging frameworks (winston, log4j, NLog)
Patterns: Factory + Builder + Decorator (combined)
The problem:
Logging systems must handle:
Why Factory + Builder + Decorator:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183
// Production-style logging system combining Factory + Builder + Decorator interface Logger { debug(message: string, meta?: object): void; info(message: string, meta?: object): void; warn(message: string, meta?: object): void; error(message: string, meta?: object): void; child(context: object): Logger; // Create child logger with added context} interface Transport { log(entry: LogEntry): void;} // Core implementationclass BaseLogger implements Logger { constructor( private context: object, private transports: Transport[], private minLevel: LogLevel ) {} private log(level: LogLevel, message: string, meta?: object): void { if (level < this.minLevel) return; const entry: LogEntry = { timestamp: new Date().toISOString(), level, message, ...this.context, ...meta, }; for (const transport of this.transports) { transport.log(entry); } } debug(message: string, meta?: object): void { this.log(LogLevel.DEBUG, message, meta); } info(message: string, meta?: object): void { this.log(LogLevel.INFO, message, meta); } warn(message: string, meta?: object): void { this.log(LogLevel.WARN, message, meta); } error(message: string, meta?: object): void { this.log(LogLevel.ERROR, message, meta); } child(context: object): Logger { return new BaseLogger( { ...this.context, ...context }, this.transports, this.minLevel ); }} // BUILDER: Configures complex logger instancesclass LoggerBuilder { private transports: Transport[] = []; private minLevel: LogLevel = LogLevel.INFO; private context: object = {}; setLevel(level: LogLevel): this { this.minLevel = level; return this; } addContext(context: object): this { this.context = { ...this.context, ...context }; return this; } addConsoleTransport(options?: ConsoleOptions): this { this.transports.push(new ConsoleTransport(options)); return this; } addFileTransport(path: string, options?: FileOptions): this { this.transports.push(new FileTransport(path, options)); return this; } addCloudTransport(config: CloudConfig): this { // DECORATOR: Wrap with batching for performance const baseTransport = new CloudTransport(config); const batchedTransport = new BatchingDecorator(baseTransport, { batchSize: 100, flushInterval: 5000, }); this.transports.push(batchedTransport); return this; } build(): Logger { if (this.transports.length === 0) { this.addConsoleTransport(); // Default } return new BaseLogger(this.context, this.transports, this.minLevel); }} // FACTORY: Creates loggers by categoryclass LoggerFactory { private rootLogger: Logger; private loggers: Map<string, Logger> = new Map(); constructor(rootLogger: Logger) { this.rootLogger = rootLogger; } getLogger(category: string): Logger { if (this.loggers.has(category)) { return this.loggers.get(category)!; } // Create child logger with category context const logger = this.rootLogger.child({ category }); this.loggers.set(category, logger); return logger; } // Static factory method for convenience static create(configure: (builder: LoggerBuilder) => void): LoggerFactory { const builder = new LoggerBuilder(); configure(builder); const rootLogger = builder.build(); return new LoggerFactory(rootLogger); }} // DECORATOR: Adds batching behavior to transportsclass BatchingDecorator implements Transport { private batch: LogEntry[] = []; private timer: NodeJS.Timer | null = null; constructor( private wrapped: Transport, private options: { batchSize: number; flushInterval: number } ) { this.startTimer(); } log(entry: LogEntry): void { this.batch.push(entry); if (this.batch.length >= this.options.batchSize) { this.flush(); } } private flush(): void { if (this.batch.length === 0) return; const entries = this.batch; this.batch = []; // Send batch to wrapped transport for (const entry of entries) { this.wrapped.log(entry); } } private startTimer(): void { this.timer = setInterval(() => this.flush(), this.options.flushInterval); }} // Usageconst loggerFactory = LoggerFactory.create(builder => { builder .setLevel(process.env.LOG_LEVEL === 'debug' ? LogLevel.DEBUG : LogLevel.INFO) .addContext({ service: 'api-server', env: process.env.NODE_ENV }) .addConsoleTransport({ colorize: true }) .addFileTransport('./logs/app.log', { maxSize: '10mb', maxFiles: 5 }) .addCloudTransport({ endpoint: process.env.LOG_ENDPOINT });}); // In services - get category-specific loggerclass UserService { private logger = loggerFactory.getLogger('UserService'); async createUser(data: CreateUserDTO): Promise<User> { this.logger.info('Creating user', { email: data.email }); // ... this.logger.debug('User created', { userId: user.id }); return user; }}This design mirrors production loggers like Winston (Node.js) and Bunyan. The combination of Factory (getLogger), Builder (configure), and Decorator (batching, formatting) creates a flexible, performant system. Child loggers with context propagation are especially valuable for request tracing.
Domain: AWS SDK, Google Cloud Client, Azure SDK
Pattern: Builder (heavily)
The problem:
Cloud SDKs must configure clients with:
Too many options for a constructor. Must support sensible defaults while allowing complete customization.
Why Builder:
Builder is the dominant pattern. AWS SDK v3, Google Cloud clients, and Azure SDK all use fluent builders extensively.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149
// AWS SDK v3-style client builder interface S3ClientConfig { region?: string; endpoint?: string; credentials?: CredentialProvider; retryMode?: 'standard' | 'adaptive'; maxAttempts?: number; requestHandler?: HttpHandler; logger?: Logger;} // Immutable configuration object - Builder produces thisclass S3Client { private readonly config: Required<S3ClientConfig>; constructor(config: S3ClientConfig) { // Resolve defaults this.config = { region: config.region ?? process.env.AWS_REGION ?? 'us-east-1', endpoint: config.endpoint ?? `https://s3.${config.region}.amazonaws.com`, credentials: config.credentials ?? defaultCredentialChain(), retryMode: config.retryMode ?? 'standard', maxAttempts: config.maxAttempts ?? 3, requestHandler: config.requestHandler ?? new FetchHttpHandler(), logger: config.logger ?? console, }; } async putObject(input: PutObjectInput): Promise<PutObjectOutput> { /* ... */ } async getObject(input: GetObjectInput): Promise<GetObjectOutput> { /* ... */ } async deleteObject(input: DeleteObjectInput): Promise<DeleteObjectOutput> { /* ... */ }} // BUILDER: Fluent configuration with validationclass S3ClientBuilder { private config: S3ClientConfig = {}; region(region: string): this { this.config.region = region; return this; } endpoint(endpoint: string): this { this.config.endpoint = endpoint; return this; } credentials(provider: CredentialProvider): this { this.config.credentials = provider; return this; } // Convenience methods for common credential types withStaticCredentials(accessKeyId: string, secretAccessKey: string): this { this.config.credentials = { accessKeyId, secretAccessKey, }; return this; } withIAMRole(roleArn: string): this { this.config.credentials = new STSAssumeRoleCredentialProvider({ roleArn, roleSessionName: 'sdk-session', }); return this; } retryMode(mode: 'standard' | 'adaptive'): this { this.config.retryMode = mode; return this; } maxAttempts(attempts: number): this { if (attempts < 1 || attempts > 10) { throw new Error('maxAttempts must be between 1 and 10'); } this.config.maxAttempts = attempts; return this; } // Custom HTTP handler for proxy, custom TLS, etc. requestHandler(handler: HttpHandler): this { this.config.requestHandler = handler; return this; } // Logging integration logger(logger: Logger): this { this.config.logger = logger; return this; } // Connect to LocalStack/MinIO for testing forLocalDevelopment(endpoint: string = 'http://localhost:4566'): this { this.config.endpoint = endpoint; this.config.credentials = { accessKeyId: 'test', secretAccessKey: 'test', }; return this; } build(): S3Client { // Validation before build if (!this.config.region && !this.config.endpoint) { throw new Error('Either region or explicit endpoint must be specified'); } return new S3Client(this.config); }} // Factory convenience for common configurationsclass S3ClientFactory { static forRegion(region: string): S3Client { return new S3ClientBuilder().region(region).build(); } static forLocalDevelopment(): S3Client { return new S3ClientBuilder().forLocalDevelopment().build(); } static customBuilder(): S3ClientBuilder { return new S3ClientBuilder(); }} // Usage examples// Simple: Factory for common caseconst s3Simple = S3ClientFactory.forRegion('us-west-2'); // Complex: Builder for full customizationconst s3Custom = S3ClientFactory.customBuilder() .region('eu-west-1') .withIAMRole('arn:aws:iam::123456789:role/S3AccessRole') .retryMode('adaptive') .maxAttempts(5) .logger(applicationLogger) .requestHandler(new NodeHttpHandler({ connectionTimeout: 5000, socketTimeout: 30000, })) .build(); // Testing: Easy local developmentconst s3Test = S3ClientFactory.forLocalDevelopment();Notice how Factory provides common shortcuts (forRegion, forLocalDevelopment) while Builder handles full customization. This layered approach serves both simple use cases and complex requirements elegantly.
Domain: ORMs, testing frameworks, seed data generation
Pattern: Factory (with Builder elements)
The problem:
Applications need to:
Manually constructing entities is tedious and error-prone, especially for entities with required relationships.
Why Factory:
Libraries like Factory Bot (Ruby), Faker.js + custom factories, and TypeORM seeders all use Factory patterns to generate entities with sensible defaults and customizable overrides.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201
// Test data factory pattern (similar to Factory Bot, faker-based factories) import { faker } from '@faker-js/faker'; // Factory for a single entity typeclass UserFactory { private overrides: Partial<User> = {}; private traits: Set<string> = new Set(); // Builder-style fluent API for customization with(overrides: Partial<User>): this { this.overrides = { ...this.overrides, ...overrides }; return this; } // Traits: predefined customization bundles admin(): this { this.traits.add('admin'); return this; } verified(): this { this.traits.add('verified'); return this; } withSubscription(): this { this.traits.add('subscription'); return this; } build(): User { const user: User = { id: faker.string.uuid(), email: faker.internet.email(), name: faker.person.fullName(), createdAt: faker.date.past(), role: 'user', verified: false, subscription: null, ...this.overrides, }; // Apply traits if (this.traits.has('admin')) { user.role = 'admin'; } if (this.traits.has('verified')) { user.verified = true; user.verifiedAt = faker.date.past(); } if (this.traits.has('subscription')) { user.subscription = SubscriptionFactory.create(); } return user; } // Create and reset for chaining create(): User { const user = this.build(); this.reset(); return user; } // Create multiple createMany(count: number): User[] { return Array.from({ length: count }, () => this.create()); } // Persist to database async createPersisted(prisma: PrismaClient): Promise<User> { const data = this.build(); const user = await prisma.user.create({ data }); this.reset(); return user; } private reset(): void { this.overrides = {}; this.traits.clear(); } // Static convenience methods static create(overrides?: Partial<User>): User { const factory = new UserFactory(); if (overrides) factory.with(overrides); return factory.create(); }} // Related entity factoryclass PostFactory { private authorId: string | null = null; private overrides: Partial<Post> = {}; by(author: User | string): this { this.authorId = typeof author === 'string' ? author : author.id; return this; } with(overrides: Partial<Post>): this { this.overrides = { ...this.overrides, ...overrides }; return this; } published(): this { return this.with({ status: 'published', publishedAt: faker.date.past() }); } draft(): this { return this.with({ status: 'draft', publishedAt: null }); } create(): Post { const post: Post = { id: faker.string.uuid(), title: faker.lorem.sentence(), content: faker.lorem.paragraphs(3), authorId: this.authorId ?? UserFactory.create().id, status: 'draft', publishedAt: null, createdAt: faker.date.past(), ...this.overrides, }; this.reset(); return post; } private reset(): void { this.authorId = null; this.overrides = {}; }} // Master factory registry for complex scenariosclass FactoryRegistry { static user(): UserFactory { return new UserFactory(); } static post(): PostFactory { return new PostFactory(); } // Create related entities in one call static async seedBlogScenario(prisma: PrismaClient): Promise<{ admin: User; authors: User[]; posts: Post[]; }> { // Create admin const admin = await FactoryRegistry.user() .admin() .verified() .createPersisted(prisma); // Create authors const authors = await Promise.all( Array.from({ length: 3 }, () => FactoryRegistry.user().verified().createPersisted(prisma) ) ); // Create posts for each author const posts: Post[] = []; for (const author of authors) { for (let i = 0; i < 5; i++) { const post = await prisma.post.create({ data: FactoryRegistry.post() .by(author) .published() .create(), }); posts.push(post); } } return { admin, authors, posts }; }} // Usage in testsdescribe('BlogService', () => { it('should return posts by author', async () => { const author = UserFactory.create(); const posts = [ PostFactory.create().by(author).published(), PostFactory.create().by(author).draft(), ]; // ... set up mocks with these entities const result = await blogService.getPostsByAuthor(author.id); expect(result).toHaveLength(2); });});Domain: HTTP libraries (Axios, fetch wrappers, got)
Patterns: Builder + Decorator + Factory (combined)
The problem:
HTTP clients need:
Different API endpoints need different configurations, but share common infrastructure.
Why Builder + Decorator + Factory:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194
// HTTP client configuration combining Builder + Decorator + Factory type Interceptor = (config: RequestConfig) => RequestConfig | Promise<RequestConfig>;type ResponseInterceptor = (response: Response) => Response | Promise<Response>; // BUILDER: Configures HTTP client instanceclass HttpClientBuilder { private baseUrl: string = ''; private headers: Record<string, string> = {}; private timeout: number = 30000; private requestInterceptors: Interceptor[] = []; private responseInterceptors: ResponseInterceptor[] = []; private retryConfig: RetryConfig | null = null; setBaseUrl(url: string): this { this.baseUrl = url; return this; } setHeader(key: string, value: string): this { this.headers[key] = value; return this; } setHeaders(headers: Record<string, string>): this { this.headers = { ...this.headers, ...headers }; return this; } setTimeout(ms: number): this { this.timeout = ms; return this; } // DECORATOR: Add request interceptor addRequestInterceptor(interceptor: Interceptor): this { this.requestInterceptors.push(interceptor); return this; } // DECORATOR: Add response interceptor addResponseInterceptor(interceptor: ResponseInterceptor): this { this.responseInterceptors.push(interceptor); return this; } // Pre-built interceptor bundles withBearerAuth(tokenProvider: () => string | Promise<string>): this { return this.addRequestInterceptor(async (config) => ({ ...config, headers: { ...config.headers, Authorization: `Bearer ${await tokenProvider()}`, }, })); } withApiKey(apiKey: string, headerName: string = 'X-API-Key'): this { return this.setHeader(headerName, apiKey); } withRetry(config: RetryConfig = { maxRetries: 3, backoff: 'exponential' }): this { this.retryConfig = config; return this; } withLogging(logger: Logger): this { return this .addRequestInterceptor((config) => { logger.debug('HTTP Request', { method: config.method, url: config.url, }); return config; }) .addResponseInterceptor((response) => { logger.debug('HTTP Response', { status: response.status, url: response.url, }); return response; }); } build(): HttpClient { return new HttpClient({ baseUrl: this.baseUrl, headers: this.headers, timeout: this.timeout, requestInterceptors: this.requestInterceptors, responseInterceptors: this.responseInterceptors, retryConfig: this.retryConfig, }); }} // FACTORY: Creates pre-configured clients for different APIsclass ApiClientFactory { constructor( private config: AppConfig, private authService: AuthService, private logger: Logger ) {} // Main API client createMainApiClient(): HttpClient { return new HttpClientBuilder() .setBaseUrl(this.config.mainApiUrl) .withBearerAuth(() => this.authService.getAccessToken()) .withRetry({ maxRetries: 3, backoff: 'exponential' }) .withLogging(this.logger) .setTimeout(10000) .build(); } // Analytics API client (different auth, longer timeout) createAnalyticsClient(): HttpClient { return new HttpClientBuilder() .setBaseUrl(this.config.analyticsApiUrl) .withApiKey(this.config.analyticsApiKey) .setTimeout(60000) // Analytics queries can be slow .withLogging(this.logger) .build(); } // Internal service client (no auth, short timeout) createInternalClient(serviceName: string): HttpClient { return new HttpClientBuilder() .setBaseUrl(`http://${serviceName}.internal:8080`) .setHeader('X-Service-Name', 'my-service') .setTimeout(5000) .withRetry({ maxRetries: 5, backoff: 'linear' }) .build(); } // Third-party API client (custom auth, rate limiting) createThirdPartyClient(): HttpClient { return new HttpClientBuilder() .setBaseUrl(this.config.thirdPartyApiUrl) .addRequestInterceptor(this.rateLimitInterceptor()) .addRequestInterceptor(this.signatureInterceptor()) .withLogging(this.logger) .build(); } private rateLimitInterceptor(): Interceptor { let lastRequest = 0; const minInterval = 100; // Max 10 req/sec return async (config) => { const now = Date.now(); const elapsed = now - lastRequest; if (elapsed < minInterval) { await sleep(minInterval - elapsed); } lastRequest = Date.now(); return config; }; } private signatureInterceptor(): Interceptor { return (config) => ({ ...config, headers: { ...config.headers, 'X-Signature': computeHmac(config.body, this.config.apiSecret), 'X-Timestamp': Date.now().toString(), }, }); }} // Usage in applicationclass OrderService { private mainApi: HttpClient; private analyticsApi: HttpClient; constructor(apiFactory: ApiClientFactory) { this.mainApi = apiFactory.createMainApiClient(); this.analyticsApi = apiFactory.createAnalyticsClient(); } async createOrder(order: CreateOrderDTO): Promise<Order> { const response = await this.mainApi.post('/orders', order); // Fire and forget analytics this.analyticsApi.post('/events', { type: 'order_created', orderId: response.data.id, }).catch(() => {}); // Don't fail order on analytics error return response.data; }}We've examined creational patterns across diverse production domains. Let's consolidate the insights:
Conclusion:
Creational patterns aren't academic exercises—they're actively shaping the software you use daily. Connection pools power your database queries, prototype cloning spawns game entities, factories create your UI components, and builders configure your cloud clients.
The patterns you've learned in this module are the same patterns used by engineers at Google, Amazon, Microsoft, and countless other organizations. You now have the conceptual framework to recognize, evaluate, and apply these patterns in your own work.
Congratulations! You've completed the module on Choosing the Right Creational Pattern. You can now compare patterns systematically, navigate decision trees to select appropriate patterns, combine patterns effectively, and recognize patterns in real-world systems. You're equipped to make informed creational design decisions in your own projects.