Loading content...
Theory crystallizes when you see patterns applied to real problems. In this page, we'll explore Factory Method across multiple domains—from document processing systems to UI frameworks, from testing infrastructure to cross-platform development.
Each example demonstrates not just how to implement Factory Method, but why it's the right choice for that particular problem. You'll see the pattern solving real architectural challenges that professional engineers encounter daily.
By the end of this page, you'll have a library of Factory Method applications you can reference. You'll recognize situations where Factory Method is the natural solution, and you'll understand how to adapt the pattern to different contexts and languages.
Let's build a production-quality document processing system that handles multiple formats. This is the classic Factory Method use case.
Build a document converter that transforms various document formats (PDF, Word, Markdown, HTML) into a common internal representation for indexing and search. The system must be extensible to support new formats without modifying existing code.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
// ==========================================// PRODUCT HIERARCHY// ========================================== interface DocumentParser { // Core parsing operations parse(content: Buffer): ParsedDocument; extractText(): string; extractMetadata(): DocumentMetadata; // Format-specific capabilities (optional) supportsImages(): boolean; extractImages?(): Image[];} class PdfParser implements DocumentParser { private pdfDoc: PDFDocument | null = null; parse(content: Buffer): ParsedDocument { // Use pdf.js or similar library this.pdfDoc = PDFDocument.load(content); return { format: 'pdf', pageCount: this.pdfDoc.getPageCount(), text: this.extractText(), metadata: this.extractMetadata(), }; } extractText(): string { if (!this.pdfDoc) throw new Error('Document not parsed'); let text = ''; for (const page of this.pdfDoc.getPages()) { text += page.getTextContent().join(''); } return text; } extractMetadata(): DocumentMetadata { const info = this.pdfDoc!.getDocumentInfo(); return { title: info.title, author: info.author, createdAt: info.creationDate, modifiedAt: info.modificationDate, }; } supportsImages(): boolean { return true; } extractImages(): Image[] { // Extract embedded images from PDF return this.pdfDoc!.getPages() .flatMap(page => page.getImages()); }} class WordParser implements DocumentParser { private docx: DocxDocument | null = null; parse(content: Buffer): ParsedDocument { // Use docx library this.docx = new DocxDocument(content); return { format: 'docx', pageCount: this.docx.estimatePageCount(), text: this.extractText(), metadata: this.extractMetadata(), }; } extractText(): string { return this.docx!.getParagraphs() .map(p => p.getText()) .join(''); } extractMetadata(): DocumentMetadata { const props = this.docx!.getCoreProperties(); return { title: props.title, author: props.creator, createdAt: props.created, modifiedAt: props.modified, }; } supportsImages(): boolean { return true; } extractImages(): Image[] { return this.docx!.getMedia() .filter(m => m.type === 'image'); }} class MarkdownParser implements DocumentParser { private content: string = ''; private ast: MarkdownAST | null = null; parse(content: Buffer): ParsedDocument { this.content = content.toString('utf-8'); this.ast = parseMarkdown(this.content); return { format: 'markdown', pageCount: 1, text: this.extractText(), metadata: this.extractMetadata(), }; } extractText(): string { // Strip markdown syntax, return plain text return this.ast!.toPlainText(); } extractMetadata(): DocumentMetadata { // Extract from YAML frontmatter if present const frontmatter = this.ast!.getFrontmatter(); return { title: frontmatter?.title, author: frontmatter?.author, createdAt: undefined, modifiedAt: undefined, }; } supportsImages(): boolean { return false; }} // ==========================================// CREATOR HIERARCHY// ========================================== abstract class DocumentConverter { // The factory method protected abstract createParser(): DocumentParser; // Template method using the factory convert(filePath: string): IndexableDocument { const content = readFileSync(filePath); const parser = this.createParser(); const parsed = parser.parse(content); const text = parser.extractText(); const metadata = parser.extractMetadata(); // Extract images if supported let images: Image[] = []; if (parser.supportsImages() && parser.extractImages) { images = parser.extractImages(); } return { id: generateDocumentId(filePath), source: filePath, format: parsed.format, content: text, metadata: metadata, images: images, indexedAt: new Date(), }; } // Batch conversion convertBatch(filePaths: string[]): IndexableDocument[] { return filePaths.map(path => this.convert(path)); }} // Concrete convertersclass PdfConverter extends DocumentConverter { protected createParser(): DocumentParser { return new PdfParser(); }} class WordConverter extends DocumentConverter { protected createParser(): DocumentParser { return new WordParser(); }} class MarkdownConverter extends DocumentConverter { protected createParser(): DocumentParser { return new MarkdownParser(); }} // ==========================================// CONFIGURATION / SELECTION// ========================================== // Factory to select appropriate converter based on file extensionfunction getConverterForFile(filePath: string): DocumentConverter { const ext = path.extname(filePath).toLowerCase(); switch (ext) { case '.pdf': return new PdfConverter(); case '.docx': case '.doc': return new WordConverter(); case '.md': case '.markdown': return new MarkdownConverter(); default: throw new Error(`Unsupported file format: ${ext}`); }} // Usageconst converter = getConverterForFile('/docs/report.pdf');const indexable = converter.convert('/docs/report.pdf');Factory Method is essential for cross-platform UI frameworks. The same application logic should produce different native UI elements on different platforms.
Build a dialog system that works on Windows, macOS, and Linux. Each platform has native dialog components with different APIs, but the application should use a unified interface.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200
// ==========================================// PRODUCT HIERARCHY: UI Components// ========================================== interface Button { render(): void; onClick(handler: () => void): void; setText(text: string): void; setEnabled(enabled: boolean): void;} interface Dialog { render(): void; addButton(button: Button): void; setTitle(title: string): void; setContent(content: string): void; show(): Promise<DialogResult>; close(): void;} interface TextField { render(): void; getValue(): string; setValue(value: string): void; setPlaceholder(text: string): void; onValueChange(handler: (value: string) => void): void;} // Windows implementationsclass WindowsButton implements Button { private hwnd: HWND; // Windows handle render(): void { this.hwnd = CreateWindowEx( WS_EX_NONE, 'BUTTON', '', WS_VISIBLE | WS_CHILD | BS_PUSHBUTTON, 0, 0, 100, 30, parentHwnd, null, hInstance, null ); } onClick(handler: () => void): void { SetWindowSubclass(this.hwnd, (msg) => { if (msg === WM_COMMAND) handler(); }); } setText(text: string): void { SetWindowText(this.hwnd, text); } setEnabled(enabled: boolean): void { EnableWindow(this.hwnd, enabled); }} class WindowsDialog implements Dialog { private hwnd: HWND; private buttons: WindowsButton[] = []; render(): void { this.hwnd = CreateWindowEx( WS_EX_DLGMODALFRAME, 'DIALOG', '', WS_POPUP | WS_CAPTION | WS_SYSMENU, 0, 0, 400, 300, null, null, hInstance, null ); } // ... implementation using Windows API} // macOS implementations class MacOSButton implements Button { private nsButton: NSButton; render(): void { this.nsButton = NSButton.alloc().initWithFrame( NSMakeRect(0, 0, 100, 30) ); this.nsButton.setBezelStyle(NSBezelStyleRounded); } onClick(handler: () => void): void { this.nsButton.setTarget(handler); this.nsButton.setAction('performClick:'); } setText(text: string): void { this.nsButton.setTitle(text); } setEnabled(enabled: boolean): void { this.nsButton.setEnabled(enabled); }} class MacOSDialog implements Dialog { private nsPanel: NSPanel; // ... implementation using Cocoa API} // Linux (GTK) implementationsclass LinuxButton implements Button { private gtkButton: GtkWidget; render(): void { this.gtkButton = gtk_button_new(); } onClick(handler: () => void): void { g_signal_connect(this.gtkButton, 'clicked', handler, null); } // ... implementation using GTK} // ==========================================// CREATOR HIERARCHY: UI Factory// ========================================== abstract class Application { // Factory methods for UI components protected abstract createButton(): Button; protected abstract createDialog(): Dialog; protected abstract createTextField(): TextField; // Application logic using factory methods showConfirmDialog(message: string): Promise<boolean> { const dialog = this.createDialog(); dialog.setTitle('Confirm'); dialog.setContent(message); const okButton = this.createButton(); okButton.setText('OK'); dialog.addButton(okButton); const cancelButton = this.createButton(); cancelButton.setText('Cancel'); dialog.addButton(cancelButton); return dialog.show().then(result => result.confirmed); } showInputDialog(prompt: string): Promise<string | null> { const dialog = this.createDialog(); dialog.setTitle('Input'); dialog.setContent(prompt); const textField = this.createTextField(); textField.setPlaceholder('Enter value...'); const okButton = this.createButton(); okButton.setText('OK'); // ... assemble and show dialog }} // Platform-specific applicationsclass WindowsApplication extends Application { protected createButton(): Button { return new WindowsButton(); } protected createDialog(): Dialog { return new WindowsDialog(); } protected createTextField(): TextField { return new WindowsTextField(); }} class MacOSApplication extends Application { protected createButton(): Button { return new MacOSButton(); } protected createDialog(): Dialog { return new MacOSDialog(); } protected createTextField(): TextField { return new MacOSTextField(); }} class LinuxApplication extends Application { protected createButton(): Button { return new LinuxButton(); } protected createDialog(): Dialog { return new LinuxDialog(); } protected createTextField(): TextField { return new LinuxTextField(); }} // ==========================================// BOOTSTRAP// ========================================== function createApplication(): Application { const platform = detectPlatform(); switch (platform) { case 'windows': return new WindowsApplication(); case 'macos': return new MacOSApplication(); case 'linux': return new LinuxApplication(); default: throw new Error(`Unsupported platform: ${platform}`); }} // The rest of the application uses the abstract Application interfaceconst app = createApplication();const confirmed = await app.showConfirmDialog('Save changes?');This pattern is used by frameworks like React Native (different renderers for iOS/Android), Electron (different dialogs on each OS), and game engines like Unity (different graphics backends). Factory Method ensures platform-specific details are isolated from application logic.
Database access layers commonly use Factory Method to create connections while supporting multiple database vendors.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208
// ==========================================// PRODUCT: Database Connection// ========================================== interface DatabaseConnection { // Connection lifecycle connect(): Promise<void>; disconnect(): Promise<void>; isConnected(): boolean; // Queries query<T>(sql: string, params?: unknown[]): Promise<T[]>; execute(sql: string, params?: unknown[]): Promise<ExecuteResult>; // Transactions beginTransaction(): Promise<Transaction>;} class PostgresConnection implements DatabaseConnection { private client: PgClient; constructor(private config: PostgresConfig) { this.client = new PgClient(config); } async connect(): Promise<void> { await this.client.connect(); } async disconnect(): Promise<void> { await this.client.end(); } isConnected(): boolean { return !this.client.ended; } async query<T>(sql: string, params?: unknown[]): Promise<T[]> { const result = await this.client.query(sql, params); return result.rows as T[]; } async execute(sql: string, params?: unknown[]): Promise<ExecuteResult> { const result = await this.client.query(sql, params); return { rowCount: result.rowCount }; } async beginTransaction(): Promise<Transaction> { await this.execute('BEGIN'); return new PostgresTransaction(this); }} class MySQLConnection implements DatabaseConnection { private connection: mysql.Connection; constructor(private config: MySQLConfig) { this.connection = mysql.createConnection(config); } async connect(): Promise<void> { return new Promise((resolve, reject) => { this.connection.connect(err => err ? reject(err) : resolve()); }); } async query<T>(sql: string, params?: unknown[]): Promise<T[]> { return new Promise((resolve, reject) => { this.connection.query(sql, params, (err, results) => { err ? reject(err) : resolve(results as T[]); }); }); } // ... other methods} class SQLiteConnection implements DatabaseConnection { private db: SQLiteDatabase; constructor(private filePath: string) {} async connect(): Promise<void> { this.db = await open({ filename: this.filePath, driver: sqlite3.Database }); } // ... SQLite-specific implementation} // ==========================================// CREATOR: Connection Pool// ========================================== abstract class ConnectionPool { private availableConnections: DatabaseConnection[] = []; private inUseConnections: Set<DatabaseConnection> = new Set(); private readonly maxSize: number; constructor(config: PoolConfig) { this.maxSize = config.maxSize || 10; } // Factory method protected abstract createConnection(): DatabaseConnection; async getConnection(): Promise<DatabaseConnection> { // Return existing available connection if (this.availableConnections.length > 0) { const conn = this.availableConnections.pop()!; this.inUseConnections.add(conn); return conn; } // Create new connection if under limit if (this.inUseConnections.size < this.maxSize) { const conn = this.createConnection(); await conn.connect(); this.inUseConnections.add(conn); return conn; } // Wait for available connection return this.waitForConnection(); } releaseConnection(conn: DatabaseConnection): void { if (this.inUseConnections.has(conn)) { this.inUseConnections.delete(conn); this.availableConnections.push(conn); } } async withConnection<T>( operation: (conn: DatabaseConnection) => Promise<T> ): Promise<T> { const conn = await this.getConnection(); try { return await operation(conn); } finally { this.releaseConnection(conn); } } async shutdown(): Promise<void> { const allConnections = [ ...this.availableConnections, ...this.inUseConnections ]; await Promise.all(allConnections.map(c => c.disconnect())); }} // Concrete poolsclass PostgresPool extends ConnectionPool { constructor(private dbConfig: PostgresConfig, poolConfig: PoolConfig) { super(poolConfig); } protected createConnection(): DatabaseConnection { return new PostgresConnection(this.dbConfig); }} class MySQLPool extends ConnectionPool { constructor(private dbConfig: MySQLConfig, poolConfig: PoolConfig) { super(poolConfig); } protected createConnection(): DatabaseConnection { return new MySQLConnection(this.dbConfig); }} class SQLitePool extends ConnectionPool { constructor(private filePath: string, poolConfig: PoolConfig) { super(poolConfig); } protected createConnection(): DatabaseConnection { return new SQLiteConnection(this.filePath); }} // ==========================================// USAGE// ========================================== // Configuration-driven pool creationfunction createPool(config: DatabaseConfig): ConnectionPool { switch (config.type) { case 'postgres': return new PostgresPool(config.postgres!, config.pool); case 'mysql': return new MySQLPool(config.mysql!, config.pool); case 'sqlite': return new SQLitePool(config.sqlite!.path, config.pool); }} // Application code is database-agnosticconst pool = createPool(appConfig.database); const users = await pool.withConnection(async (conn) => { return conn.query<User>('SELECT * FROM users WHERE active = $1', [true]);});Testing frameworks use Factory Method to create test runners, reporters, and assertions that work across different testing paradigms.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207
// ==========================================// PRODUCT: Test Reporter// ========================================== interface TestReporter { onSuiteStart(suite: TestSuite): void; onSuiteEnd(suite: TestSuite, result: SuiteResult): void; onTestStart(test: TestCase): void; onTestEnd(test: TestCase, result: TestResult): void; onComplete(summary: TestSummary): void;} class ConsoleReporter implements TestReporter { onSuiteStart(suite: TestSuite): void { console.log(`📦 ${suite.name}`); } onTestStart(test: TestCase): void { process.stdout.write(` ⏳ ${test.name}...`); } onTestEnd(test: TestCase, result: TestResult): void { if (result.passed) { console.log(` ✅ (${result.duration}ms)`); } else { console.log(` ❌`); console.log(` ${result.error?.message}`); } } onComplete(summary: TestSummary): void { console.log(`${summary.passed} passed, ${summary.failed} failed`); }} class JUnitXmlReporter implements TestReporter { private xmlDoc: XmlDocument; constructor(private outputPath: string) { this.xmlDoc = new XmlDocument(); } onTestEnd(test: TestCase, result: TestResult): void { const testCase = this.xmlDoc.createElement('testcase'); testCase.setAttribute('name', test.name); testCase.setAttribute('time', result.duration.toString()); if (!result.passed) { const failure = this.xmlDoc.createElement('failure'); failure.setAttribute('message', result.error?.message || ''); testCase.appendChild(failure); } this.currentSuite.appendChild(testCase); } onComplete(summary: TestSummary): void { writeFileSync(this.outputPath, this.xmlDoc.toString()); } // ... other methods} class JsonReporter implements TestReporter { private results: TestResultJson = { suites: [] }; onComplete(summary: TestSummary): void { this.results.summary = summary; console.log(JSON.stringify(this.results, null, 2)); } // ... collects JSON-formatted results} // ==========================================// CREATOR: Test Runner// ========================================== abstract class TestRunner { protected suites: TestSuite[] = []; // Factory methods protected abstract createReporter(): TestReporter; protected abstract createTestEnvironment(): TestEnvironment; addSuite(suite: TestSuite): void { this.suites.push(suite); } async run(): Promise<TestSummary> { const reporter = this.createReporter(); const env = this.createTestEnvironment(); const summary: TestSummary = { passed: 0, failed: 0, skipped: 0 }; for (const suite of this.suites) { reporter.onSuiteStart(suite); // Set up environment for suite await env.setup(); for (const test of suite.tests) { reporter.onTestStart(test); const result = await this.runTest(test, env); if (result.passed) { summary.passed++; } else { summary.failed++; } reporter.onTestEnd(test, result); } await env.teardown(); reporter.onSuiteEnd(suite, { tests: suite.tests.length }); } reporter.onComplete(summary); return summary; } private async runTest( test: TestCase, env: TestEnvironment ): Promise<TestResult> { const start = performance.now(); try { await env.beforeEach(); await test.fn(); await env.afterEach(); return { passed: true, duration: performance.now() - start, }; } catch (error) { return { passed: false, duration: performance.now() - start, error: error as Error, }; } }} // Concrete runners for different use casesclass DevelopmentTestRunner extends TestRunner { protected createReporter(): TestReporter { return new ConsoleReporter(); } protected createTestEnvironment(): TestEnvironment { return new LocalTestEnvironment(); }} class CITestRunner extends TestRunner { constructor(private outputDir: string) { super(); } protected createReporter(): TestReporter { return new JUnitXmlReporter( path.join(this.outputDir, 'test-results.xml') ); } protected createTestEnvironment(): TestEnvironment { return new DockerTestEnvironment(); }} class WatchModeRunner extends TestRunner { protected createReporter(): TestReporter { // Minimal reporter for speed return new MinimalReporter(); } protected createTestEnvironment(): TestEnvironment { // Reusable environment for speed return new PersistentTestEnvironment(); }} // ==========================================// USAGE// ========================================== function createTestRunner(): TestRunner { if (process.env.CI) { return new CITestRunner('./test-output'); } else if (process.argv.includes('--watch')) { return new WatchModeRunner(); } else { return new DevelopmentTestRunner(); }} const runner = createTestRunner();runner.addSuite(userTests);runner.addSuite(orderTests);await runner.run();Production logging systems often need different outputs for different environments. Factory Method makes this clean and extensible.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215
// ==========================================// PRODUCT: Log Writer// ========================================== interface LogWriter { write(entry: LogEntry): void; flush(): Promise<void>; close(): Promise<void>;} interface LogEntry { level: LogLevel; message: string; timestamp: Date; context?: Record<string, unknown>;} class ConsoleLogWriter implements LogWriter { write(entry: LogEntry): void { const formatted = `[${entry.level}] ${entry.message}`; switch (entry.level) { case LogLevel.ERROR: console.error(formatted, entry.context); break; case LogLevel.WARN: console.warn(formatted, entry.context); break; default: console.log(formatted, entry.context); } } async flush(): Promise<void> { /* no-op for console */ } async close(): Promise<void> { /* no-op for console */ }} class FileLogWriter implements LogWriter { private buffer: LogEntry[] = []; private stream: WriteStream; constructor(filePath: string) { this.stream = createWriteStream(filePath, { flags: 'a' }); } write(entry: LogEntry): void { const line = JSON.stringify({ ...entry, timestamp: entry.timestamp.toISOString(), }); this.stream.write(line + ''); } async flush(): Promise<void> { return new Promise(resolve => this.stream.once('drain', resolve)); } async close(): Promise<void> { return new Promise(resolve => this.stream.end(resolve)); }} class CloudWatchLogWriter implements LogWriter { private logs: CloudWatchLogs; private batch: LogEntry[] = []; private readonly batchSize = 100; constructor(private logGroup: string, private logStream: string) { this.logs = new CloudWatchLogs(); } write(entry: LogEntry): void { this.batch.push(entry); if (this.batch.length >= this.batchSize) { this.sendBatch(); } } private async sendBatch(): Promise<void> { const events = this.batch.map(entry => ({ timestamp: entry.timestamp.getTime(), message: JSON.stringify(entry), })); await this.logs.putLogEvents({ logGroupName: this.logGroup, logStreamName: this.logStream, logEvents: events, }); this.batch = []; } async flush(): Promise<void> { if (this.batch.length > 0) { await this.sendBatch(); } } async close(): Promise<void> { await this.flush(); }} // ==========================================// CREATOR: Logger// ========================================== abstract class Logger { private minLevel: LogLevel = LogLevel.DEBUG; private writer: LogWriter | null = null; // Factory method protected abstract createWriter(): LogWriter; private getWriter(): LogWriter { if (!this.writer) { this.writer = this.createWriter(); } return this.writer; } setMinLevel(level: LogLevel): void { this.minLevel = level; } log(level: LogLevel, message: string, context?: Record<string, unknown>): void { if (level < this.minLevel) return; const entry: LogEntry = { level, message, timestamp: new Date(), context, }; this.getWriter().write(entry); } // Convenience methods debug(msg: string, ctx?: Record<string, unknown>): void { this.log(LogLevel.DEBUG, msg, ctx); } info(msg: string, ctx?: Record<string, unknown>): void { this.log(LogLevel.INFO, msg, ctx); } warn(msg: string, ctx?: Record<string, unknown>): void { this.log(LogLevel.WARN, msg, ctx); } error(msg: string, ctx?: Record<string, unknown>): void { this.log(LogLevel.ERROR, msg, ctx); } async shutdown(): Promise<void> { const writer = this.getWriter(); await writer.flush(); await writer.close(); }} // Concrete loggersclass DevelopmentLogger extends Logger { protected createWriter(): LogWriter { return new ConsoleLogWriter(); }} class FileLogger extends Logger { constructor(private logDir: string) { super(); } protected createWriter(): LogWriter { const filename = `app-${new Date().toISOString().split('T')[0]}.log`; return new FileLogWriter(path.join(this.logDir, filename)); }} class ProductionLogger extends Logger { constructor( private logGroup: string, private instanceId: string ) { super(); } protected createWriter(): LogWriter { return new CloudWatchLogWriter(this.logGroup, this.instanceId); }} class TestLogger extends Logger { private entries: LogEntry[] = []; protected createWriter(): LogWriter { return { write: (entry) => this.entries.push(entry), flush: async () => {}, close: async () => {}, }; } getEntries(): LogEntry[] { return this.entries; } clear(): void { this.entries = []; }}A notification system that sends messages through different channels (email, SMS, push, Slack) is a perfect Factory Method candidate.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188
// ==========================================// PRODUCT: Notification Sender// ========================================== interface NotificationSender { send(recipient: string, message: NotificationMessage): Promise<SendResult>; supportsRichContent(): boolean; getMaxMessageLength(): number;} interface NotificationMessage { subject?: string; body: string; htmlBody?: string; attachments?: Attachment[]; priority: 'low' | 'normal' | 'high';} class EmailSender implements NotificationSender { constructor(private transport: NodeMailerTransport) {} async send(recipient: string, message: NotificationMessage): Promise<SendResult> { const result = await this.transport.sendMail({ to: recipient, subject: message.subject || 'Notification', text: message.body, html: message.htmlBody, attachments: message.attachments?.map(a => ({ filename: a.name, content: a.content, })), }); return { success: true, messageId: result.messageId }; } supportsRichContent(): boolean { return true; } getMaxMessageLength(): number { return Infinity; }} class SmsSender implements NotificationSender { constructor(private twilioClient: TwilioClient) {} async send(recipient: string, message: NotificationMessage): Promise<SendResult> { // SMS doesn't support rich content - use plain body const body = message.body.substring(0, 160); const result = await this.twilioClient.messages.create({ to: recipient, from: this.fromNumber, body: body, }); return { success: true, messageId: result.sid }; } supportsRichContent(): boolean { return false; } getMaxMessageLength(): number { return 160; }} class SlackSender implements NotificationSender { constructor(private webhookUrl: string) {} async send(recipient: string, message: NotificationMessage): Promise<SendResult> { const blocks = this.formatAsBlocks(message); await fetch(this.webhookUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ channel: recipient, blocks: blocks, }), }); return { success: true }; } supportsRichContent(): boolean { return true; } getMaxMessageLength(): number { return 4000; }} // ==========================================// CREATOR: Notification Service// ========================================== abstract class NotificationService { private rateLimiter = new RateLimiter(); // Factory method protected abstract createSender(): NotificationSender; async notify( recipient: string, message: NotificationMessage ): Promise<NotificationResult> { // Rate limiting if (await this.rateLimiter.isRateLimited(recipient)) { return { success: false, error: 'Rate limited' }; } const sender = this.createSender(); // Prepare message based on sender capabilities const preparedMessage = this.prepareMessage(message, sender); try { const result = await sender.send(recipient, preparedMessage); await this.logNotification(recipient, result); return { success: true, ...result }; } catch (error) { await this.logError(recipient, error as Error); return { success: false, error: (error as Error).message }; } } private prepareMessage( message: NotificationMessage, sender: NotificationSender ): NotificationMessage { // Truncate if needed let body = message.body; const maxLength = sender.getMaxMessageLength(); if (body.length > maxLength) { body = body.substring(0, maxLength - 3) + '...'; } // Remove rich content if not supported if (!sender.supportsRichContent()) { return { ...message, body, htmlBody: undefined, attachments: undefined }; } return { ...message, body }; }} // Concrete notification servicesclass EmailNotificationService extends NotificationService { constructor(private smtpConfig: SmtpConfig) { super(); } protected createSender(): NotificationSender { const transport = createTransport(this.smtpConfig); return new EmailSender(transport); }} class SmsNotificationService extends NotificationService { constructor( private accountSid: string, private authToken: string ) { super(); } protected createSender(): NotificationSender { const client = new TwilioClient(this.accountSid, this.authToken); return new SmsSender(client); }} class SlackNotificationService extends NotificationService { constructor(private webhookUrl: string) { super(); } protected createSender(): NotificationSender { return new SlackSender(this.webhookUrl); }} // Test notification serviceclass MockNotificationService extends NotificationService { public sentNotifications: Array<{ recipient: string; message: NotificationMessage }> = []; protected createSender(): NotificationSender { return { send: async (recipient, message) => { this.sentNotifications.push({ recipient, message }); return { success: true, messageId: 'mock-id' }; }, supportsRichContent: () => true, getMaxMessageLength: () => Infinity, }; }}Factory Method is widely used in popular frameworks and libraries. Recognizing it helps you understand and extend these tools.
| Framework/Library | Factory Method Location | What It Creates |
|---|---|---|
| React | React.createElement() | Virtual DOM elements |
| Angular | ComponentFactoryResolver | Component instances |
| Java Collections | iterator() | Iterator objects |
| JDBC | DriverManager.getConnection() | Database connections |
| Hibernate | SessionFactory.openSession() | Database sessions |
| Spring | @Bean methods | Configured bean instances |
| Python logging | getLogger() | Logger instances |
| Node.js http | http.createServer() | Server instances |
| .NET | WebApplication.CreateBuilder() | Application builder |
Example: Java's Iterator Pattern uses Factory Method
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960
// Collection is the Creatorpublic interface Collection<E> { // This is a factory method! Iterator<E> iterator();} // ArrayList is a ConcreteCreatorpublic class ArrayList<E> implements Collection<E> { @Override public Iterator<E> iterator() { // Returns a ConcreteProduct return new ArrayListIterator(); } // ConcreteProduct - private inner class private class ArrayListIterator implements Iterator<E> { private int cursor = 0; @Override public boolean hasNext() { return cursor < size(); } @Override public E next() { return get(cursor++); } }} // LinkedList is another ConcreteCreatorpublic class LinkedList<E> implements Collection<E> { @Override public Iterator<E> iterator() { return new LinkedListIterator(); } private class LinkedListIterator implements Iterator<E> { private Node<E> current = head; @Override public boolean hasNext() { return current != null; } @Override public E next() { E data = current.data; current = current.next; return data; } }} // Client code uses the factory method polymorphicallyCollection<String> collection = getCollection(); // ArrayList or LinkedListIterator<String> iterator = collection.iterator(); // Factory method callwhile (iterator.hasNext()) { System.out.println(iterator.next());}Any method named 'create...', 'make...', 'build...', 'get...', or 'new...' that returns an interface type and can be overridden is likely a Factory Method. Learning to spot the pattern helps you leverage frameworks more effectively.
We've explored Factory Method across multiple domains. Let's identify the common patterns:
You've now completed the Factory Method Pattern module! You understand the problem (direct instantiation coupling), the solution (defer creation to subclasses), the structure (Creator, Product, ConcreteCreator, ConcreteProduct), and real-world applications. You're ready to recognize and apply Factory Method in your own projects.