Design Pattern 26: Template Method Pattern - Complete Guide with Real-World Examples
π Download the complete Design Pattern series code from our design_pattern repository.
π― What is the Template Method Pattern?
The Template Method Pattern is a behavioral design pattern that defines the skeleton of an algorithm in a base class, allowing subclasses to override specific steps without changing the algorithmβs structure. This pattern promotes code reuse and ensures consistent algorithm execution while providing flexibility for customization.
Key Benefits:
- β Code reuse - Common algorithm structure shared across subclasses
- β Consistent execution - Algorithm flow remains the same
- β Flexibility - Subclasses can customize specific steps
- β Maintainability - Changes to algorithm structure affect all subclasses
- β Extensibility - Easy to add new algorithm variations
π Real-World Problem: Data Format Conversion System
Letβs design a data format conversion system with the following requirements:
System Requirements:
- Support multiple format conversions (JSON, XML, CSV, YAML)
- Maintain consistent conversion workflow across all formats
- Easy extensibility for new formats
- Avoid code duplication in conversion logic
- Handle different data sources (files, databases, APIs)
Business Rules:
- All conversions follow the same 3-step process: Read β Format β Output
- Each format has specific formatting rules
- System should handle errors gracefully
- Performance optimization for large datasets
- Support for validation and transformation
ποΈ Object-Oriented Analysis (OOA)
Letβs analyze the problem and identify the core components:

Identified Forces:
- Code Duplication
- Each format conversion implements the same 3-step process
- Common logic repeated across multiple classes
- Violation of Open-Closed Principle (OCP)
- Adding new formats requires modifying existing code
- Changes to conversion workflow affect all implementations
- Maintenance Complexity
- Conversion logic scattered across multiple classes
- Difficult to ensure consistency across all formats
- Inconsistent Error Handling
- Each format handles errors differently
- No standardized validation approach
π‘ Template Method Pattern Solution
After analyzing the forces, we can apply the Template Method Pattern to create a flexible conversion framework:

Template Method Pattern Components:
- Abstract Class
- Defines the template method with algorithm skeleton
- Provides default implementations for common steps
- Declares abstract methods for customizable steps
- Concrete Classes
- Inherit from abstract class
- Implement specific formatting logic
- Can override default implementations if needed
- Template Method
- Orchestrates the algorithm execution
- Ensures consistent workflow
- Handles common operations
Benefits:
- Consistent algorithm structure across all implementations
- Code reuse through shared template method
- Flexible customization through method overriding
- Easy maintenance and extension
π οΈ Implementation: Data Format Conversion System
Hereβs the complete implementation using the Template Method Pattern:

1. Abstract Base Class
abstract class DataFormatter {
// Template method - defines the algorithm structure
fun convert(data: Map<String, Any>): ConversionResult {
return try {
val rawData = readData(data)
val validatedData = validateData(rawData)
val formattedData = formatData(validatedData)
val result = outputData(formattedData)
ConversionResult.Success(result, getFormatType())
} catch (e: Exception) {
ConversionResult.Error("Conversion failed: ${e.message}", getFormatType())
}
}
// Hook method - can be overridden by subclasses
protected open fun validateData(data: String): String {
return data.trim()
}
// Common implementation - shared by all subclasses
private fun readData(data: Map<String, Any>): String {
return data.entries.joinToString(", ") { "${it.key}=${it.value}" }
}
// Abstract methods - must be implemented by subclasses
protected abstract fun formatData(data: String): String
protected abstract fun outputData(data: String): String
protected abstract fun getFormatType(): String
// Optional hook method for performance optimization
protected open fun shouldOptimize(): Boolean = false
}
// Result classes for better error handling
sealed class ConversionResult {
data class Success(val data: String, val format: String) : ConversionResult()
data class Error(val message: String, val format: String) : ConversionResult()
}
2. Concrete Implementations
class JsonFormatter : DataFormatter() {
override fun formatData(data: String): String {
val entries = data.split(", ").associate { entry ->
val (key, value) = entry.split("=", limit = 2)
key to value
}
return buildJsonObject {
entries.forEach { (key, value) ->
put(key, value)
}
}.toString()
}
override fun outputData(data: String): String {
return if (shouldOptimize()) {
"JSON (Optimized): $data"
} else {
"JSON Output: $data"
}
}
override fun getFormatType(): String = "JSON"
override fun shouldOptimize(): Boolean = true
}
class XmlFormatter : DataFormatter() {
override fun formatData(data: String): String {
val entries = data.split(", ").associate { entry ->
val (key, value) = entry.split("=", limit = 2)
key to value
}
return buildString {
appendLine("<?xml version=\"1.0\" encoding=\"UTF-8\"?>")
appendLine("<data>")
entries.forEach { (key, value) ->
appendLine(" <$key>$value</$key>")
}
append("</data>")
}
}
override fun outputData(data: String): String {
return "XML Output: $data"
}
override fun getFormatType(): String = "XML"
override fun validateData(data: String): String {
// XML-specific validation
return data.replace("<", "<").replace(">", ">")
}
}
class CsvFormatter : DataFormatter() {
override fun formatData(data: String): String {
val entries = data.split(", ").map { entry ->
val (key, value) = entry.split("=", limit = 2)
"$key,$value"
}
return buildString {
appendLine("key,value") // Header
entries.forEach { entry ->
appendLine(entry)
}
}
}
override fun outputData(data: String): String {
return "CSV Output: $data"
}
override fun getFormatType(): String = "CSV"
}
class YamlFormatter : DataFormatter() {
override fun formatData(data: String): String {
val entries = data.split(", ").associate { entry ->
val (key, value) = entry.split("=", limit = 2)
key to value
}
return buildString {
entries.forEach { (key, value) ->
appendLine("$key: $value")
}
}
}
override fun outputData(data: String): String {
return "YAML Output: $data"
}
override fun getFormatType(): String = "YAML"
}
3. Advanced Implementation with Hooks
abstract class AdvancedDataFormatter : DataFormatter() {
// Template method with additional hooks
fun convertWithMetadata(data: Map<String, Any>): ConversionResult {
val startTime = System.currentTimeMillis()
val result = convert(data)
val endTime = System.currentTimeMillis()
val processingTime = endTime - startTime
return when (result) {
is ConversionResult.Success -> {
ConversionResult.Success(
"${result.data}\n<!-- Processing time: ${processingTime}ms -->",
result.format
)
}
is ConversionResult.Error -> result
}
}
// Hook method for preprocessing
protected open fun preprocess(data: Map<String, Any>): Map<String, Any> {
return data
}
// Hook method for postprocessing
protected open fun postprocess(formattedData: String): String {
return formattedData
}
}
class OptimizedJsonFormatter : AdvancedDataFormatter() {
override fun preprocess(data: Map<String, Any>): Map<String, Any> {
// Remove null values and sort keys
return data.filterValues { it != null }
.toSortedMap()
}
override fun postprocess(formattedData: String): String {
// Minify JSON
return formattedData.replace(Regex("\\s+"), "")
}
override fun formatData(data: String): String {
// Implementation similar to JsonFormatter but optimized
return super.formatData(data)
}
override fun outputData(data: String): String {
return "Optimized JSON: $data"
}
override fun getFormatType(): String = "Optimized JSON"
}
4. Client Code
fun main() {
println("=== Data Format Conversion Demo ===")
val data = mapOf(
"name" to "John Doe",
"age" to 30,
"city" to "New York",
"occupation" to "Software Engineer"
)
// Test different formatters
val formatters = listOf(
JsonFormatter(),
XmlFormatter(),
CsvFormatter(),
YamlFormatter(),
OptimizedJsonFormatter()
)
formatters.forEach { formatter ->
println("\n--- ${formatter.javaClass.simpleName} ---")
val result = formatter.convert(data)
when (result) {
is ConversionResult.Success -> {
println("β
${result.format} conversion successful:")
println(result.data)
}
is ConversionResult.Error -> {
println("β ${result.format} conversion failed:")
println(result.message)
}
}
}
// Test with metadata
println("\n--- Advanced Formatter with Metadata ---")
val advancedFormatter = OptimizedJsonFormatter()
val advancedResult = advancedFormatter.convertWithMetadata(data)
when (advancedResult) {
is ConversionResult.Success -> {
println("β
Advanced conversion successful:")
println(advancedResult.data)
}
is ConversionResult.Error -> {
println("β Advanced conversion failed:")
println(advancedResult.message)
}
}
}
Expected Output:
=== Data Format Conversion Demo ===
--- JsonFormatter ---
β
JSON conversion successful:
JSON (Optimized): {"age":"30","city":"New York","name":"John Doe","occupation":"Software Engineer"}
--- XmlFormatter ---
β
XML conversion successful:
XML Output: <?xml version="1.0" encoding="UTF-8"?>
<data>
<age>30</age>
<city>New York</city>
<name>John Doe</name>
<occupation>Software Engineer</occupation>
</data>
--- CsvFormatter ---
β
CSV conversion successful:
CSV Output: key,value
name,John Doe
age,30
city,New York
occupation,Software Engineer
--- YamlFormatter ---
β
YAML conversion successful:
YAML Output: name: John Doe
age: 30
city: New York
occupation: Software Engineer
--- OptimizedJsonFormatter ---
β
Optimized JSON conversion successful:
Optimized JSON: {"age":"30","city":"New York","name":"John Doe","occupation":"Software Engineer"}
--- Advanced Formatter with Metadata ---
β
Advanced conversion successful:
Optimized JSON: {"age":"30","city":"New York","name":"John Doe","occupation":"Software Engineer"}
<!-- Processing time: 15ms -->
π§ Advanced Template Method Patterns
1. Template Method with Strategy Pattern
interface ValidationStrategy {
fun validate(data: String): Boolean
}
class StrictValidationStrategy : ValidationStrategy {
override fun validate(data: String): Boolean {
return data.isNotEmpty() && data.length < 1000
}
}
class LenientValidationStrategy : ValidationStrategy {
override fun validate(data: String): Boolean {
return data.isNotEmpty()
}
}
abstract class ConfigurableDataFormatter(
private val validationStrategy: ValidationStrategy
) : DataFormatter() {
override fun validateData(data: String): String {
return if (validationStrategy.validate(data)) {
data.trim()
} else {
throw IllegalArgumentException("Data validation failed")
}
}
}
2. Template Method with Factory Pattern
object DataFormatterFactory {
fun createFormatter(format: String): DataFormatter {
return when (format.lowercase()) {
"json" -> JsonFormatter()
"xml" -> XmlFormatter()
"csv" -> CsvFormatter()
"yaml" -> YamlFormatter()
else -> throw IllegalArgumentException("Unsupported format: $format")
}
}
}
// Usage
val formatter = DataFormatterFactory.createFormatter("json")
val result = formatter.convert(data)
3. Template Method with Observer Pattern
interface ConversionObserver {
fun onConversionStart(format: String)
fun onConversionComplete(format: String, result: ConversionResult)
fun onConversionError(format: String, error: String)
}
abstract class ObservableDataFormatter : DataFormatter() {
private val observers = mutableListOf<ConversionObserver>()
fun addObserver(observer: ConversionObserver) {
observers.add(observer)
}
override fun convert(data: Map<String, Any>): ConversionResult {
observers.forEach { it.onConversionStart(getFormatType()) }
return try {
val result = super.convert(data)
observers.forEach { it.onConversionComplete(getFormatType(), result) }
result
} catch (e: Exception) {
val errorResult = ConversionResult.Error(e.message ?: "Unknown error", getFormatType())
observers.forEach { it.onConversionError(getFormatType(), e.message ?: "Unknown error") }
errorResult
}
}
}
π Template Method Pattern vs Alternative Approaches
Approach | Pros | Cons |
---|---|---|
Template Method | β
Code reuse β Consistent structure β Easy extension | β Inheritance coupling β Limited flexibility |
Strategy Pattern | β
Runtime flexibility β No inheritance | β No shared structure β More complex setup |
Command Pattern | β
Undo/redo support β Queue operations | β Overkill for simple algorithms β Complex implementation |
Function Composition | β
Functional approach β High flexibility | β No enforced structure β Learning curve |
π― When to Use the Template Method Pattern
β Perfect For:
- Algorithm frameworks with consistent structure
- Data processing pipelines (ETL, format conversion)
- Document generation (reports, exports)
- Build processes (compilation, deployment)
- Test frameworks (setup β test β teardown)
- Workflow engines (approval processes, workflows)
β Avoid When:
- Simple one-off algorithms (use direct implementation)
- Highly variable algorithms (use Strategy pattern)
- Runtime algorithm selection (use Strategy pattern)
- Multiple inheritance needed (use composition)
π Related Design Patterns
- Strategy Pattern: For runtime algorithm selection
- Factory Method: For creating algorithm instances
- Command Pattern: For encapsulating algorithm execution
- Chain of Responsibility: For step-by-step processing
π Real-World Applications
1. Build Systems
abstract class BuildProcess {
fun build(): BuildResult {
clean()
compile()
test()
package()
return deploy()
}
protected abstract fun clean()
protected abstract fun compile()
protected abstract fun test()
protected abstract fun package()
protected abstract fun deploy(): BuildResult
}
class JavaBuildProcess : BuildProcess() {
override fun clean() = println("Cleaning Java project...")
override fun compile() = println("Compiling Java sources...")
override fun test() = println("Running JUnit tests...")
override fun package() = println("Creating JAR file...")
override fun deploy(): BuildResult = BuildResult.Success("Deployed to Maven Central")
}
2. Database Operations
abstract class DatabaseOperation<T> {
fun execute(): OperationResult<T> {
connect()
val result = performOperation()
commit()
disconnect()
return result
}
protected abstract fun connect()
protected abstract fun performOperation(): OperationResult<T>
protected abstract fun commit()
protected abstract fun disconnect()
}
class UserInsertOperation(private val user: User) : DatabaseOperation<User>() {
override fun connect() = println("Connecting to database...")
override fun performOperation(): OperationResult<User> {
println("Inserting user: ${user.name}")
return OperationResult.Success(user)
}
override fun commit() = println("Committing transaction...")
override fun disconnect() = println("Disconnecting from database...")
}
3. Web Request Processing
abstract class RequestHandler {
fun handle(request: Request): Response {
authenticate(request)
authorize(request)
val response = processRequest(request)
log(request, response)
return response
}
protected abstract fun authenticate(request: Request)
protected abstract fun authorize(request: Request)
protected abstract fun processRequest(request: Request): Response
protected abstract fun log(request: Request, response: Response)
}
class ApiRequestHandler : RequestHandler() {
override fun authenticate(request: Request) = println("Validating API key...")
override fun authorize(request: Request) = println("Checking permissions...")
override fun processRequest(request: Request): Response = Response("API Response")
override fun log(request: Request, response: Response) = println("Logging API call...")
}
4. Test Frameworks
abstract class TestCase {
fun run(): TestResult {
setUp()
val result = runTest()
tearDown()
return result
}
protected abstract fun setUp()
protected abstract fun runTest(): TestResult
protected abstract fun tearDown()
}
class UserServiceTest : TestCase() {
override fun setUp() = println("Setting up test database...")
override fun runTest(): TestResult = TestResult.Passed("User creation test passed")
override fun tearDown() = println("Cleaning up test data...")
}
π¨ Common Pitfalls and Best Practices
1. Overuse of Template Methods
// β Avoid: Too many template methods
abstract class BadTemplate {
fun method1() { /* template */ }
fun method2() { /* template */ }
fun method3() { /* template */ }
// ... many more
}
// β
Prefer: Focus on core algorithm
abstract class GoodTemplate {
fun execute() { /* main template */ }
protected abstract fun step1()
protected abstract fun step2()
}
2. Inappropriate Hook Methods
// β Avoid: Too many hooks making it complex
abstract class ComplexTemplate {
protected open fun hook1() { /* default */ }
protected open fun hook2() { /* default */ }
protected open fun hook3() { /* default */ }
// ... many hooks
}
// β
Prefer: Minimal, meaningful hooks
abstract class SimpleTemplate {
protected open fun validate() { /* default validation */ }
protected abstract fun process()
}
3. Proper Error Handling
// β
Good: Comprehensive error handling
abstract class RobustTemplate {
fun execute(): Result {
return try {
val result = performAlgorithm()
Result.Success(result)
} catch (e: Exception) {
Result.Error(e.message ?: "Unknown error")
}
}
protected abstract fun performAlgorithm(): Any
}
π Related Articles
- Design Pattern 1: Object-Oriented Concepts
- Design Pattern 2: Design Principles
- Strategy Pattern
- Factory Method Pattern
- Command Pattern
β Conclusion
Through the Template Method Pattern, we successfully created a flexible data format conversion system that maintains consistent workflow while allowing customization of specific steps.
Key Advantages:
- π― Code reuse - Common algorithm structure shared across implementations
- π§ Consistent execution - Algorithm flow remains the same for all formats
- π Easy extension - New formats can be added without modifying existing code
- π‘οΈ Maintainability - Changes to algorithm structure affect all subclasses
- β‘ Performance optimization - Hook methods allow for format-specific optimizations
Design Principles Followed:
- Single Responsibility Principle (SRP): Each class handles one format conversion
- Open-Closed Principle (OCP): Open for extension (new formats), closed for modification
- Donβt Repeat Yourself (DRY): Common logic shared in template method
- Template Method Pattern: Defines algorithm skeleton with customizable steps
Perfect For:
- Data processing pipelines (ETL, format conversion, validation)
- Build systems (compilation, testing, deployment)
- Workflow engines (approval processes, business processes)
- Test frameworks (setup β test β teardown)
- Document generation (reports, exports, templates)
The Template Method Pattern provides an elegant solution for creating reusable algorithm frameworks while maintaining flexibility and consistency!
π‘ Pro Tip: Use hook methods sparingly and only when they provide meaningful customization points. Too many hooks can make the template method complex and hard to understand.
π Stay Updated: Follow our Design Pattern series for more software architecture insights!
Enjoy Reading This Article?
Here are some more articles you might like to read next: