🍵 Analogy
Making a hot drink follows a fixed recipe: boil water, brew, pour into a cup, add condiments. Tea and coffee differ only in how they brew and what condiments they add — the steps and their order never change. Template Method is that recipe card: the skeleton is fixed, two steps are left blank for each drink to fill in.
The problem
Sending a one-time passcode is always the same dance: generate the code, cache it, build a message, send a notification. Only a couple of steps differ between SMS and email. If you copy the whole sequence into each channel, the shared order drifts and bugs creep in. Template Method writes the sequence once and lets each channel supply just the steps that vary.
Structure
classDiagram
class OTPFlow {
-steps OTPSteps
+GenAndSend(length) "fixed skeleton"
}
class OTPSteps {
<<interface>>
+genOTP(length)
+saveCache(otp)
+message(otp)
+send(msg)
}
class SMS {
+genOTP() +saveCache() +message() +send()
}
class Email {
+genOTP() +saveCache() +message() +send()
}
OTPFlow o--> OTPSteps : calls the varying steps
OTPSteps <|.. SMS
OTPSteps <|.. Email
Idiomatic Go
Go has no abstract base class, so the “template method” lives on a struct that holds an interface of the varying steps — an OTP/iOTP-style design. The fixed sequence calls into that interface. Edit and Run:
package main
import "fmt"
// The steps that vary between channels.
type OTPSteps interface {
genOTP(length int) string
saveCache(otp string)
message(otp string) string
send(msg string) error
}
// Template method: the fixed sequence, written exactly once.
type OTPFlow struct{ steps OTPSteps }
func (f OTPFlow) GenAndSend(length int) error {
otp := f.steps.genOTP(length) // varies
f.steps.saveCache(otp) // varies
msg := f.steps.message(otp) // varies
fmt.Println(msg)
return f.steps.send(msg) // varies
}
type SMS struct{}
func (SMS) genOTP(n int) string { return "1234" }
func (SMS) saveCache(otp string) { fmt.Println("SMS OTP cached") }
func (SMS) message(otp string) string { return "Your SMS login code is " + otp }
func (SMS) send(msg string) error { fmt.Println("sent via SMS"); return nil }
type Email struct{}
func (Email) genOTP(n int) string { return "5678" }
func (Email) saveCache(otp string) { fmt.Println("Email OTP cached") }
func (Email) message(otp string) string { return "Your email login code is " + otp }
func (Email) send(msg string) error { fmt.Println("sent via Email"); return nil }
func main() {
OTPFlow{steps: SMS{}}.GenAndSend(4)
fmt.Println("---")
OTPFlow{steps: Email{}}.GenAndSend(4)
}
🐹 Two Go flavors — interface or function hooks
One version embeds the base OTP in each channel; injecting the steps as an interface field (above) is the cleaner variant. For just one or two varying steps, you can skip the interface entirely and pass them as function fields on the struct — same pattern, less ceremony. Either way, the fixed flow stays in one place.
In the standard library
sort.Sort— the sorting algorithm is the fixed skeleton; yourLen/Less/Swapare the varying steps.text/templateexecution — a fixed render loop calls your funcs and data.testing— the test runner’s setup → run → teardown flow with yourTestXxxbody as the variable step.
Pitfalls
⚠️ Watch the line with Strategy
If you find yourself letting callers replace the whole procedure, you’ve drifted into Strategy. Template Method’s value is that it owns the flow and only opens specific holes. If there’s no meaningful fixed skeleton, don’t force one.
When to use it — and when not
✅ Reach for it when
- Several variants share the same overall procedure but differ in a few steps (SMS vs email OTP, tea vs coffee).
- You want to write the invariant sequence once and guarantee its order.
- You want to let callers customize specific steps without touching the orchestration.
⛔ Think twice when
- There's only one variant — there's no skeleton worth abstracting.
- The steps don't really share a fixed order; Strategy or plain functions fit better.
Related patterns
Define a family of interchangeable algorithms, encapsulate each one, and select which to use at runtime.
CREATIONAL Factory MethodDefine an interface for creating an object, but let the implementation decide which concrete type to instantiate.
BEHAVIORAL CommandTurn a request into a standalone object, letting you parameterize, queue, log, and undo operations.
Check your understanding
Score: 0 / 31. What does Template Method fix, and what does it vary?
The template method holds the invariant sequence; subtypes (or injected step implementations) fill in the parts that differ.
2. Go has no inheritance — how is Template Method expressed idiomatically?
Instead of an abstract base class with overridable hooks, you inject the varying steps as an interface (or as function fields) and the fixed sequence calls into them.
3. How does Template Method differ from Strategy?
Template Method keeps control of the overall flow and lets you fill in blanks; Strategy hands the entire algorithm over to an interchangeable object.