Loading learning content...
The Template Method Pattern isn't just a theoretical construct—it's one of the most widely-deployed patterns in professional software development. Every major framework you've used likely employs Template Method internally, and many APIs you interact with are designed around this pattern.
In this page, we'll explore concrete, production-quality examples across multiple domains. By studying these implementations, you'll develop the pattern recognition needed to apply Template Method effectively in your own projects.
By the end of this page, you will have seen Template Method applied in testing frameworks, web request handling, data processing pipelines, game development, and more. You'll understand how to identify when Template Method is the right choice and have implementation blueprints you can adapt.
Testing frameworks like JUnit, pytest, and Jest are among the most recognizable implementations of the Template Method Pattern. The test execution lifecycle follows a fixed structure, but the actual test logic varies per test class.
The template:
Let's implement a simplified testing framework that demonstrates this pattern:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185
// ============================================// Abstract Test Case: The Template// ============================================ abstract class TestCase { private testResults: TestResult[] = []; /** * TEMPLATE METHOD: Runs all tests in this test case * * Defines the test execution lifecycle. Subclasses implement * individual lifecycle hooks and test methods. */ public async runTests(): Promise<TestSuiteResult> { const testMethods = this.discoverTests(); console.log(`📋 Running ${this.constructor.name}`); console.log(` Found ${testMethods.length} tests`); // HOOK: Suite-level setup await this.beforeAll(); for (const testMethod of testMethods) { await this.runSingleTest(testMethod); } // HOOK: Suite-level teardown await this.afterAll(); return this.compileResults(); } /** * TEMPLATE METHOD: Runs a single test with setup/teardown */ private async runSingleTest(testMethod: string): Promise<void> { const startTime = performance.now(); let status: 'passed' | 'failed' | 'error' = 'passed'; let errorMessage: string | undefined; try { // HOOK: Individual test setup await this.beforeEach(); // PRIMITIVE: Execute the actual test await (this as any)[testMethod](); } catch (error) { status = error instanceof AssertionError ? 'failed' : 'error'; errorMessage = String(error); } finally { try { // HOOK: Individual test teardown (always runs) await this.afterEach(); } catch (teardownError) { console.warn(`Teardown failed: ${teardownError}`); } } const duration = performance.now() - startTime; this.recordResult(testMethod, status, duration, errorMessage); } // ============================================ // HOOK METHODS: Optional overrides // ============================================ /** Hook: Called once before all tests. Override for suite setup. */ protected async beforeAll(): Promise<void> { // Default: no-op } /** Hook: Called once after all tests. Override for suite cleanup. */ protected async afterAll(): Promise<void> { // Default: no-op } /** Hook: Called before each test. Override for per-test setup. */ protected async beforeEach(): Promise<void> { // Default: no-op } /** Hook: Called after each test. Override for per-test cleanup. */ protected async afterEach(): Promise<void> { // Default: no-op } // ============================================ // CONCRETE METHODS: Shared logic // ============================================ private discoverTests(): string[] { // Find all methods starting with "test" return Object.getOwnPropertyNames(Object.getPrototypeOf(this)) .filter(name => name.startsWith('test') && typeof (this as any)[name] === 'function'); } private recordResult(name: string, status: string, duration: number, error?: string): void { const emoji = status === 'passed' ? '✓' : status === 'failed' ? '✗' : '⚠'; console.log(` ${emoji} ${name} (${duration.toFixed(2)}ms)`); if (error) console.log(` └─ ${error}`); this.testResults.push({ name, status, duration, error } as TestResult); } private compileResults(): TestSuiteResult { return { suiteName: this.constructor.name, results: this.testResults, passed: this.testResults.filter(r => r.status === 'passed').length, failed: this.testResults.filter(r => r.status !== 'passed').length, }; }} // ============================================// Concrete Test Cases: User Implementations// ============================================ class UserServiceTests extends TestCase { private db!: TestDatabase; private userService!: UserService; // Override hook: suite-level setup protected async beforeAll(): Promise<void> { this.db = await TestDatabase.create(); console.log(' 📁 Database created'); } // Override hook: suite-level cleanup protected async afterAll(): Promise<void> { await this.db.destroy(); console.log(' 📁 Database destroyed'); } // Override hook: per-test setup protected async beforeEach(): Promise<void> { await this.db.clear(); this.userService = new UserService(this.db); } // Test methods (discovered automatically) async testCreateUser(): Promise<void> { const user = await this.userService.create({ name: 'Alice', email: 'alice@example.com' }); assertEqual(user.name, 'Alice'); assertEqual(user.email, 'alice@example.com'); assertDefined(user.id); } async testFindUserById(): Promise<void> { const created = await this.userService.create({ name: 'Bob', email: 'bob@example.com' }); const found = await this.userService.findById(created.id); assertEqual(found?.name, 'Bob'); assertEqual(found?.id, created.id); } async testDeleteUser(): Promise<void> { const user = await this.userService.create({ name: 'Charlie', email: 'charlie@example.com' }); await this.userService.delete(user.id); const found = await this.userService.findById(user.id); assertEqual(found, null); }} // ============================================// Running Tests// ============================================ async function runAllTests(): Promise<void> { const suites = [ new UserServiceTests(), new ProductServiceTests(), new OrderServiceTests(), ]; for (const suite of suites) { const results = await suite.runTests(); console.log(` Summary: ${results.passed} passed, ${results.failed} failed`); }}This is exactly how JUnit, pytest, and similar frameworks work. They define the test lifecycle (template method), and you implement your tests (primitive operations) and lifecycle hooks (optional overrides). You never call the test runner—it calls you.
Web frameworks process HTTP requests through a standardized pipeline. The Template Method Pattern defines this pipeline while allowing customization at specific points.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207
// ============================================// Abstract Request Handler: The Template// ============================================ abstract class RequestHandler { private logger: Logger; private metrics: MetricsCollector; constructor(logger: Logger, metrics: MetricsCollector) { this.logger = logger; this.metrics = metrics; } /** * TEMPLATE METHOD: Process an incoming HTTP request * * Defines the request processing pipeline. Subclasses implement * specific steps like authentication, validation, and business logic. */ public async handleRequest(request: HttpRequest): Promise<HttpResponse> { const requestId = this.generateRequestId(); const startTime = Date.now(); this.logger.info(`[${requestId}] Request received: ${request.method} ${request.path}`); try { // CONCRETE: Parse request body const parsedRequest = await this.parseRequest(request); // PRIMITIVE: Authenticate (must be implemented) const authContext = await this.authenticate(parsedRequest); // PRIMITIVE: Authorize (must be implemented) await this.authorize(authContext, parsedRequest); // PRIMITIVE: Validate input (must be implemented) const validatedInput = await this.validateInput(parsedRequest); // HOOK: Pre-processing (optional) await this.beforeHandle(validatedInput, authContext); // PRIMITIVE: Execute business logic (must be implemented) const result = await this.handle(validatedInput, authContext); // HOOK: Post-processing (optional) const processedResult = await this.afterHandle(result, authContext); // PRIMITIVE: Format response (must be implemented) const response = this.formatResponse(processedResult); this.metrics.recordSuccess(this.getHandlerName(), Date.now() - startTime); return response; } catch (error) { this.logger.error(`[${requestId}] Error: ${error}`); this.metrics.recordError(this.getHandlerName(), error); // HOOK: Error handling (can be overridden) return this.handleError(error); } } // ============================================ // PRIMITIVE OPERATIONS: Must implement // ============================================ protected abstract authenticate(request: ParsedRequest): Promise<AuthContext>; protected abstract authorize(auth: AuthContext, request: ParsedRequest): Promise<void>; protected abstract validateInput(request: ParsedRequest): Promise<ValidatedInput>; protected abstract handle(input: ValidatedInput, auth: AuthContext): Promise<any>; protected abstract formatResponse(result: any): HttpResponse; /** Returns the handler name for metrics/logging */ protected abstract getHandlerName(): string; // ============================================ // HOOK OPERATIONS: Optional overrides // ============================================ protected async beforeHandle(input: ValidatedInput, auth: AuthContext): Promise<void> { // Default: no pre-processing } protected async afterHandle(result: any, auth: AuthContext): Promise<any> { // Default: return result unchanged return result; } protected handleError(error: unknown): HttpResponse { // Default error handling if (error instanceof AuthenticationError) { return { status: 401, body: { error: 'Unauthorized' } }; } if (error instanceof AuthorizationError) { return { status: 403, body: { error: 'Forbidden' } }; } if (error instanceof ValidationError) { return { status: 400, body: { error: 'Invalid input', details: error.details } }; } return { status: 500, body: { error: 'Internal server error' } }; } // ============================================ // CONCRETE OPERATIONS: Shared logic // ============================================ private generateRequestId(): string { return `req_${Date.now()}_${Math.random().toString(36).slice(2)}`; } private async parseRequest(request: HttpRequest): Promise<ParsedRequest> { return { method: request.method, path: request.path, headers: request.headers, query: this.parseQueryString(request.query), body: request.body ? JSON.parse(request.body) : {}, }; } private parseQueryString(query: string): Record<string, string> { // Parse query string implementation return {}; }} // ============================================// Concrete Handler: Create User Endpoint// ============================================ class CreateUserHandler extends RequestHandler { private userService: UserService; private tokenService: TokenService; constructor(deps: { userService: UserService; tokenService: TokenService; logger: Logger; metrics: MetricsCollector }) { super(deps.logger, deps.metrics); this.userService = deps.userService; this.tokenService = deps.tokenService; } protected getHandlerName(): string { return 'CreateUserHandler'; } protected async authenticate(request: ParsedRequest): Promise<AuthContext> { const token = request.headers['authorization']?.replace('Bearer ', ''); if (!token) { throw new AuthenticationError('Missing authentication token'); } return this.tokenService.verify(token); } protected async authorize(auth: AuthContext, request: ParsedRequest): Promise<void> { if (!auth.permissions.includes('users:create')) { throw new AuthorizationError('Insufficient permissions to create users'); } } protected async validateInput(request: ParsedRequest): Promise<ValidatedInput> { const { name, email, role } = request.body; if (!name || typeof name !== 'string' || name.length < 2) { throw new ValidationError('Invalid name', { field: 'name' }); } if (!email || !email.includes('@')) { throw new ValidationError('Invalid email', { field: 'email' }); } if (role && !['admin', 'user', 'guest'].includes(role)) { throw new ValidationError('Invalid role', { field: 'role' }); } return { name, email, role: role || 'user' }; } protected async handle(input: ValidatedInput, auth: AuthContext): Promise<User> { return this.userService.create({ name: input.name, email: input.email, role: input.role, createdBy: auth.userId, }); } protected formatResponse(user: User): HttpResponse { return { status: 201, body: { id: user.id, name: user.name, email: user.email, role: user.role, createdAt: user.createdAt.toISOString(), }, }; } // Override hook for audit logging protected async afterHandle(user: User, auth: AuthContext): Promise<User> { await this.auditLog.record({ action: 'user_created', performedBy: auth.userId, targetId: user.id, timestamp: new Date(), }); return user; }}This pattern is used in frameworks like Spring MVC (AbstractController), ASP.NET MVC (Controller base class), and Django's class-based views. Each provides a pipeline template that you customize for specific endpoints.
ETL (Extract-Transform-Load) pipelines are a natural fit for Template Method. The pipeline structure is consistent, but each data source and destination requires specific handling.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240
// ============================================// Abstract ETL Pipeline: The Template// ============================================ abstract class ETLPipeline<TSource, TTransformed, TDestination> { private logger: Logger; private retryPolicy: RetryPolicy; constructor(logger: Logger, retryPolicy: RetryPolicy) { this.logger = logger; this.retryPolicy = retryPolicy; } /** * TEMPLATE METHOD: Execute the ETL pipeline */ public async execute(): Promise<PipelineResult> { const stats: PipelineStats = { startTime: new Date(), extractedCount: 0, transformedCount: 0, loadedCount: 0, errorCount: 0, }; this.logger.info(`Starting ETL pipeline: ${this.getPipelineName()}`); try { // HOOK: Pre-extraction preparation await this.beforeExtract(); // PRIMITIVE: Connect to source const sourceConnection = await this.connectToSource(); // PRIMITIVE: Extract data with batching const batches = await this.extractBatches(sourceConnection); // PRIMITIVE: Connect to destination const destConnection = await this.connectToDestination(); for await (const batch of batches) { stats.extractedCount += batch.length; try { // HOOK: Pre-transform validation const validBatch = await this.validateExtracted(batch); // PRIMITIVE: Transform data const transformed = await this.transform(validBatch); stats.transformedCount += transformed.length; // HOOK: Pre-load preparation const prepared = await this.prepareForLoad(transformed); // PRIMITIVE: Load to destination await this.load(destConnection, prepared); stats.loadedCount += prepared.length; } catch (batchError) { stats.errorCount += batch.length; // HOOK: Error handling per batch await this.handleBatchError(batch, batchError); } } // HOOK: Post-load cleanup await this.afterLoad(); // CONCRETE: Close connections await this.closeConnections(sourceConnection, destConnection); } catch (pipelineError) { this.logger.error(`Pipeline failed: ${pipelineError}`); // HOOK: Fatal error handling await this.handleFatalError(pipelineError); throw pipelineError; } stats.endTime = new Date(); return { stats, success: stats.errorCount === 0 }; } // ============================================ // PRIMITIVE OPERATIONS: Must implement // ============================================ protected abstract getPipelineName(): string; protected abstract connectToSource(): Promise<TSourceConnection>; protected abstract extractBatches(connection: TSourceConnection): AsyncGenerator<TSource[]>; protected abstract connectToDestination(): Promise<TDestConnection>; protected abstract transform(data: TSource[]): Promise<TTransformed[]>; protected abstract load(connection: TDestConnection, data: TTransformed[]): Promise<void>; // ============================================ // HOOK OPERATIONS: Optional overrides // ============================================ protected async beforeExtract(): Promise<void> {} protected async validateExtracted(batch: TSource[]): Promise<TSource[]> { // Default: all records are valid return batch; } protected async prepareForLoad(data: TTransformed[]): Promise<TTransformed[]> { // Default: no additional preparation return data; } protected async afterLoad(): Promise<void> {} protected async handleBatchError(batch: TSource[], error: unknown): Promise<void> { this.logger.error(`Batch failed with ${batch.length} records: ${error}`); // Default: log and continue } protected async handleFatalError(error: unknown): Promise<void> { // Default: just log } // ============================================ // CONCRETE OPERATIONS: Shared logic // ============================================ private async closeConnections(...connections: any[]): Promise<void> { for (const conn of connections) { try { await conn?.close(); } catch (e) { this.logger.warn(`Failed to close connection: ${e}`); } } }} // ============================================// Concrete Pipeline: MySQL to ElasticSearch// ============================================ interface MySqlProduct { id: number; name: string; description: string; price: number; category_id: number; created_at: Date;} interface ElasticProduct { id: string; name: string; description: string; price: number; category: string; searchableText: string; timestamp: string;} class ProductSearchIndexPipeline extends ETLPipeline<MySqlProduct, ElasticProduct, void> { private mysql: MySqlClient; private elastic: ElasticClient; private categoryMap: Map<number, string> = new Map(); protected getPipelineName(): string { return 'ProductSearchIndexer'; } // Override hook: load category lookup before extraction protected async beforeExtract(): Promise<void> { const categories = await this.mysql.query('SELECT id, name FROM categories'); for (const cat of categories) { this.categoryMap.set(cat.id, cat.name); } } protected async connectToSource(): Promise<MySqlConnection> { return this.mysql.getConnection(); } protected async *extractBatches(connection: MySqlConnection): AsyncGenerator<MySqlProduct[]> { const BATCH_SIZE = 1000; let offset = 0; while (true) { const batch = await connection.query<MySqlProduct>( 'SELECT * FROM products ORDER BY id LIMIT ? OFFSET ?', [BATCH_SIZE, offset] ); if (batch.length === 0) break; yield batch; offset += BATCH_SIZE; } } protected async connectToDestination(): Promise<ElasticConnection> { return this.elastic.getConnection(); } protected async transform(products: MySqlProduct[]): Promise<ElasticProduct[]> { return products.map(p => ({ id: `product_${p.id}`, name: p.name, description: p.description, price: p.price, category: this.categoryMap.get(p.category_id) || 'Unknown', searchableText: `${p.name} ${p.description}`.toLowerCase(), timestamp: new Date().toISOString(), })); } protected async load(connection: ElasticConnection, products: ElasticProduct[]): Promise<void> { await connection.bulk({ index: 'products', body: products.flatMap(p => [ { index: { _id: p.id } }, p ]) }); } // Override hook: validate products before transform protected async validateExtracted(batch: MySqlProduct[]): Promise<MySqlProduct[]> { return batch.filter(p => { if (!p.name || p.price < 0) { this.logger.warn(`Skipping invalid product: ${p.id}`); return false; } return true; }); } // Override hook: refresh search index after load protected async afterLoad(): Promise<void> { await this.elastic.indices.refresh({ index: 'products' }); }}Game development extensively uses Template Method for game loops, entity behavior, and state machines. The game framework defines the update cycle; games customize what happens each frame.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255
// ============================================// Abstract Game Entity: The Template// ============================================ abstract class GameEntity { protected position: Vector2; protected velocity: Vector2; protected isActive: boolean = true; protected health: number; protected maxHealth: number; constructor(initialPosition: Vector2) { this.position = initialPosition; this.velocity = { x: 0, y: 0 }; this.maxHealth = this.getMaxHealth(); this.health = this.maxHealth; } /** * TEMPLATE METHOD: Main update loop called every frame */ public update(deltaTime: number, gameState: GameState): void { if (!this.isActive) return; // HOOK: Pre-update logic this.onPreUpdate(deltaTime, gameState); // PRIMITIVE: Update AI/behavior this.updateBehavior(deltaTime, gameState); // CONCRETE: Apply physics this.applyPhysics(deltaTime); // PRIMITIVE: Check collisions this.handleCollisions(gameState.entities); // HOOK: Post-update logic this.onPostUpdate(deltaTime, gameState); // CONCRETE: Check death condition if (this.health <= 0) { this.onDeath(); this.isActive = false; } } /** * TEMPLATE METHOD: Render the entity */ public render(renderer: Renderer): void { if (!this.isActive) return; // HOOK: Pre-render effects this.onPreRender(renderer); // PRIMITIVE: Draw the entity this.draw(renderer); // HOOK: Post-render effects (particles, etc.) this.onPostRender(renderer); // HOOK: Render health bar if applicable if (this.shouldShowHealthBar()) { this.renderHealthBar(renderer); } } // ============================================ // PRIMITIVE OPERATIONS: Must implement // ============================================ protected abstract getMaxHealth(): number; protected abstract updateBehavior(deltaTime: number, gameState: GameState): void; protected abstract draw(renderer: Renderer): void; protected abstract getCollisionRadius(): number; // ============================================ // HOOK OPERATIONS: Optional overrides // ============================================ protected onPreUpdate(deltaTime: number, gameState: GameState): void {} protected onPostUpdate(deltaTime: number, gameState: GameState): void {} protected onPreRender(renderer: Renderer): void {} protected onPostRender(renderer: Renderer): void {} protected onDeath(): void { // Default: just log console.log(`${this.constructor.name} died at ${this.position.x}, ${this.position.y}`); } protected shouldShowHealthBar(): boolean { return this.health < this.maxHealth; } protected onCollision(other: GameEntity): void { // Default: no collision response } // ============================================ // CONCRETE OPERATIONS: Shared logic // ============================================ private applyPhysics(deltaTime: number): void { this.position.x += this.velocity.x * deltaTime; this.position.y += this.velocity.y * deltaTime; // Apply friction this.velocity.x *= 0.98; this.velocity.y *= 0.98; } private handleCollisions(entities: GameEntity[]): void { for (const other of entities) { if (other === this || !other.isActive) continue; const distance = this.distanceTo(other); const collisionDistance = this.getCollisionRadius() + other.getCollisionRadius(); if (distance < collisionDistance) { this.onCollision(other); other.onCollision(this); } } } private renderHealthBar(renderer: Renderer): void { const barWidth = 40; const healthPercent = this.health / this.maxHealth; renderer.drawRect( this.position.x - barWidth / 2, this.position.y - 20, barWidth * healthPercent, 5, healthPercent > 0.5 ? 'green' : healthPercent > 0.25 ? 'yellow' : 'red' ); } private distanceTo(other: GameEntity): number { const dx = this.position.x - other.position.x; const dy = this.position.y - other.position.y; return Math.sqrt(dx * dx + dy * dy); } public takeDamage(amount: number): void { this.health = Math.max(0, this.health - amount); }} // ============================================// Concrete Entities: Game-Specific// ============================================ class Player extends GameEntity { private inputManager: InputManager; private inventory: Inventory; private moveSpeed = 200; protected getMaxHealth(): number { return 100; } protected getCollisionRadius(): number { return 16; } protected updateBehavior(deltaTime: number, gameState: GameState): void { // Player behavior: respond to input const input = this.inputManager.getState(); this.velocity.x = 0; this.velocity.y = 0; if (input.left) this.velocity.x = -this.moveSpeed; if (input.right) this.velocity.x = this.moveSpeed; if (input.up) this.velocity.y = -this.moveSpeed; if (input.down) this.velocity.y = this.moveSpeed; // Attack if (input.attack && this.canAttack()) { this.performAttack(gameState); } } protected draw(renderer: Renderer): void { renderer.drawSprite('player', this.position.x, this.position.y); } protected onDeath(): void { // Custom death behavior: trigger game over super.onDeath(); GameEvents.emit('playerDeath'); } protected shouldShowHealthBar(): boolean { return true; // Always show player health }} class Enemy extends GameEntity { private target: GameEntity | null = null; private patrolPath: Vector2[]; private currentPatrolIndex = 0; private attackCooldown = 0; protected getMaxHealth(): number { return 50; } protected getCollisionRadius(): number { return 12; } protected updateBehavior(deltaTime: number, gameState: GameState): void { // Find player as target this.target = gameState.entities.find(e => e instanceof Player) || null; if (this.target && this.distanceTo(this.target) < 150) { // Chase player this.chaseTarget(); } else { // Patrol this.patrol(); } this.attackCooldown = Math.max(0, this.attackCooldown - deltaTime); } protected draw(renderer: Renderer): void { renderer.drawSprite('enemy', this.position.x, this.position.y); } protected onCollision(other: GameEntity): void { if (other instanceof Player && this.attackCooldown <= 0) { other.takeDamage(10); this.attackCooldown = 1.0; // 1 second cooldown } } protected onDeath(): void { super.onDeath(); // Drop loot GameEvents.emit('enemyDropLoot', { position: this.position, enemyType: this.constructor.name }); } // Override hook: particle effects on hit protected onPostRender(renderer: Renderer): void { if (this.wasRecentlyDamaged()) { renderer.drawParticles('damage', this.position); } } private chaseTarget(): void { /* ... */ } private patrol(): void { /* ... */ }}Build systems like Maven, Gradle, and npm scripts use Template Method to define build lifecycles. Each project type (Java, Kotlin, TypeScript) customizes specific build steps while maintaining a consistent pipeline.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222
// ============================================// Abstract Build Pipeline: The Template// ============================================ abstract class BuildPipeline { protected projectRoot: string; protected outputDir: string; protected logger: BuildLogger; constructor(projectRoot: string, outputDir: string, logger: BuildLogger) { this.projectRoot = projectRoot; this.outputDir = outputDir; this.logger = logger; } /** * TEMPLATE METHOD: Execute full build pipeline * * This follows the Maven lifecycle pattern: * validate → compile → test → package → verify → install → deploy */ public async build(options: BuildOptions): Promise<BuildResult> { const artifacts: Artifact[] = []; const reports: TestReport[] = []; try { this.logger.phase('VALIDATE'); await this.validate(); this.logger.phase('INITIALIZE'); await this.initialize(); this.logger.phase('RESOLVE DEPENDENCIES'); await this.resolveDependencies(); this.logger.phase('COMPILE'); const compiledFiles = await this.compile(); if (options.runTests) { this.logger.phase('TEST'); const testReport = await this.runTests(); reports.push(testReport); if (testReport.failed > 0 && !options.continueOnTestFailure) { throw new TestFailureError(`${testReport.failed} tests failed`); } } if (options.package) { this.logger.phase('PACKAGE'); const artifact = await this.package(compiledFiles); artifacts.push(artifact); } // HOOK: Custom post-build steps await this.afterBuild(artifacts); return { success: true, artifacts, reports }; } catch (error) { this.logger.error(`Build failed: ${error}`); return { success: false, error: String(error), artifacts, reports }; } } // ============================================ // PRIMITIVE OPERATIONS: Must implement // ============================================ protected abstract compile(): Promise<CompiledFile[]>; protected abstract package(files: CompiledFile[]): Promise<Artifact>; protected abstract runTests(): Promise<TestReport>; protected abstract getDependencyResolver(): DependencyResolver; // ============================================ // HOOK OPERATIONS: Optional overrides // ============================================ protected async validate(): Promise<void> { // Default: check project structure exists if (!await this.fileExists(this.projectRoot)) { throw new Error('Project root not found'); } } protected async initialize(): Promise<void> { // Default: create output directory await this.ensureDir(this.outputDir); } protected async resolveDependencies(): Promise<void> { const resolver = this.getDependencyResolver(); await resolver.resolve(); } protected async afterBuild(artifacts: Artifact[]): Promise<void> { // Default: no post-build steps } // ============================================ // CONCRETE OPERATIONS: Shared utilities // ============================================ protected async fileExists(path: string): Promise<boolean> { try { await fs.access(path); return true; } catch { return false; } } protected async ensureDir(path: string): Promise<void> { await fs.mkdir(path, { recursive: true }); }} // ============================================// Concrete: TypeScript Build Pipeline// ============================================ class TypeScriptBuildPipeline extends BuildPipeline { private tsConfig: TSConfig; protected async compile(): Promise<CompiledFile[]> { this.logger.info('Compiling TypeScript...'); // Run TypeScript compiler const result = await this.exec('npx tsc'); if (result.exitCode !== 0) { throw new CompilationError(result.stderr); } // Find all generated JS files return this.findFiles(this.outputDir, '**/*.js'); } protected async runTests(): Promise<TestReport> { this.logger.info('Running Jest tests...'); const result = await this.exec('npx jest --json'); return this.parseJestOutput(result.stdout); } protected async package(files: CompiledFile[]): Promise<Artifact> { this.logger.info('Creating npm package...'); await this.exec('npm pack'); const packageJson = await this.readJson('package.json'); const tarballName = `${packageJson.name}-${packageJson.version}.tgz`; return { name: packageJson.name, version: packageJson.version, path: path.join(this.projectRoot, tarballName), type: 'npm-package', }; } protected getDependencyResolver(): DependencyResolver { return new NpmDependencyResolver(this.projectRoot); } // Override validate to check tsconfig protected async validate(): Promise<void> { await super.validate(); if (!await this.fileExists(path.join(this.projectRoot, 'tsconfig.json'))) { throw new Error('tsconfig.json not found'); } } // Override afterBuild for TypeScript-specific steps protected async afterBuild(artifacts: Artifact[]): Promise<void> { // Generate type declarations await this.exec('npx tsc --declaration --emitDeclarationOnly'); this.logger.info('Type declarations generated'); }} // ============================================// Concrete: Rust Build Pipeline// ============================================ class RustBuildPipeline extends BuildPipeline { protected async compile(): Promise<CompiledFile[]> { this.logger.info('Compiling Rust with Cargo...'); const result = await this.exec('cargo build --release'); if (result.exitCode !== 0) { throw new CompilationError(result.stderr); } return this.findFiles(path.join(this.outputDir, 'release'), '*'); } protected async runTests(): Promise<TestReport> { this.logger.info('Running Cargo tests...'); const result = await this.exec('cargo test -- --format json'); return this.parseCargoTestOutput(result.stdout); } protected async package(files: CompiledFile[]): Promise<Artifact> { const cargoToml = await this.readToml('Cargo.toml'); const binaryPath = path.join(this.outputDir, 'release', cargoToml.package.name); return { name: cargoToml.package.name, version: cargoToml.package.version, path: binaryPath, type: 'binary', }; } protected getDependencyResolver(): DependencyResolver { return new CargoDependencyResolver(this.projectRoot); }}After seeing multiple use cases, let's consolidate when Template Method is the right choice and when alternative patterns might be better suited:
| Situation | Recommended Pattern |
|---|---|
| Fixed algorithm, varying steps, class-based variations | Template Method |
| Interchangeable algorithms selected at runtime | Strategy |
| Adding behavior dynamically, composable | Decorator |
| Sequential steps with composition | Chain of Responsibility |
| Complex object construction | Builder |
| State-dependent behavior changes | State |
Template Method often works alongside other patterns. You might use Strategy for specific steps within a template method (e.g., different validation strategies), or use Factory Method to create objects needed by the algorithm. Patterns are tools that work together.
Even when Template Method is the right choice, implementation mistakes can undermine its benefits. Here are common anti-patterns to avoid:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051
// BEFORE: Too many abstract methods (anti-pattern)abstract class ReportGenerator { public generate(): Report { this.gatherData(); this.formatTitle(); // Abstract this.formatSubtitle(); // Abstract this.formatHeader(); // Abstract this.formatFooter(); // Abstract this.formatPageNumbers(); // Abstract this.formatBorders(); // Abstract this.renderBody(); // Abstract this.applyStyles(); // Abstract this.addWatermark(); // Abstract return this.finalize(); } // 9 abstract methods = excessive burden on subclasses protected abstract formatTitle(): void; protected abstract formatSubtitle(): void; protected abstract formatHeader(): void; protected abstract formatFooter(): void; protected abstract formatPageNumbers(): void; protected abstract formatBorders(): void; protected abstract renderBody(): void; protected abstract applyStyles(): void; protected abstract addWatermark(): void;} // AFTER: Grouped operations, fewer abstracts, more hooksabstract class ReportGenerator { public generate(): Report { this.gatherData(); this.configureFormatting(this.getFormattingOptions()); // One config object this.renderContent(this.getContentRenderer()); // Strategy injection this.applyFinishing(); // Hook return this.finalize(); } // Only 2 abstract methods protected abstract getFormattingOptions(): FormattingOptions; protected abstract getContentRenderer(): ContentRenderer; // Rest are hooks with sensible defaults protected applyFinishing(): void { // Default: no watermark, standard borders }} // FormattingOptions handles title, subtitle, header, footer, etc.// ContentRenderer handles body rendering// Much easier for subclasses to implementWe've explored the Template Method Pattern through comprehensive real-world examples. Let's consolidate the key patterns and insights:
| Aspect | Guidance |
|---|---|
| When to use | Fixed algorithm structure, varying implementations at specific steps |
| Abstract methods | Use for mandatory customization points (2-5 is ideal) |
| Hooks | Use for optional customization with sensible defaults |
| Template method | Keep it focused and non-overridable (final if possible) |
| Testing | Test primitive operations independently; test algorithm flow once |
| Avoid | Too many abstracts, deep hierarchies, subclass calling template |
You have completed the Template Method Pattern module! You understand the problem it solves (algorithms with varying steps), the solution architecture (abstract class with template method), the underlying principle (Hollywood), and can apply it in real-world scenarios. You're now equipped to recognize opportunities for this pattern and implement it correctly.