The Problem
Several workflows may share the same broad algorithm but differ in a few steps. Batch jobs, import pipelines, and sync tasks often repeat the same fetch-transform-persist structure. Duplicating the whole algorithm just to vary one stage makes the shared flow harder to maintain.
The Solution
Template Method fixes the high-level sequence while allowing specific steps to vary. In Go, this is often expressed without inheritance by using a runner function plus a small interface of hooks. The overall algorithm stays in one place, and concrete jobs provide only the variable steps.
Structure
The Template Runner
RunSync is the template. It defines the fixed algorithm sequence: FetchRecords → Transform → Persist. The order and the shared error handling logic never change regardless of which job is passed in.
flowchart TD Client["Client"] Runner["RunSync template runner"] Hooks["SyncHooks interface"] InventoryJob["InventorySyncJob"] CatalogJob["CatalogSyncJob"] Client -->|"RunSync(job)"| Runner Runner -->|"FetchRecords()"| Hooks Runner -->|"Transform()"| Hooks Runner -->|"Persist()"| Hooks InventoryJob -.->|"implements"| Hooks CatalogJob -.->|"implements"| Hooks
- Template runner:
RunSyncdefines the fixed algorithm sequence. - Hook interface:
SyncHooksdeclares the steps that can vary. - Concrete jobs: Inventory and catalog sync jobs provide different fetch and transform logic.
- Client: Passes a concrete job into the runner.
Implementation
This example runs a sync pipeline with a shared algorithm: fetch records, transform them, and persist the output. Different jobs customize the step behavior without rewriting the pipeline skeleton.
package main
import "fmt"
type SyncHooks interface {
Name() string
Fetch() []string
Transform(records []string) []string
Persist(records []string) error
}
func RunSync(job SyncHooks) error {
fmt.Println("running", job.Name())
records := job.Fetch()
transformed := job.Transform(records)
return job.Persist(transformed)
} Real-World Analogy
Think of a baking recipe. The overall sequence stays fixed: prepare, mix, bake, cool. Different recipes vary the ingredients or timings of specific steps, but they still follow the same skeleton.
Pros and Cons
| Pros | Cons |
|---|---|
| Keeps the shared workflow in one place. | The fixed skeleton can become too rigid as workflows diverge. |
| Reduces duplication across similar pipelines. | Hook contracts can become awkward if too many steps vary. |
| Lets implementations customize only the steps that vary. | Less flexible than Strategy when the whole algorithm should change. |
Best Practices
- Keep the fixed algorithm in one place so shared workflow changes happen once.
- Use a small hook interface that reflects only the steps that vary.
- In Go, prefer composition and function-driven hooks over inheritance-heavy designs.
- Reserve Template Method for workflows with a real stable skeleton, not just loosely similar functions.
- If the algorithm itself should vary, Strategy is usually a better fit.
When to Use
- Several workflows share the same ordered pipeline but differ in a few steps.
- You want the common sequence enforced consistently.
- Concrete jobs should customize behavior without duplicating the full algorithm.
When NOT to Use
- The steps do not actually follow one stable skeleton.
- Each workflow varies so much that a shared template becomes brittle.
- A plain helper function or Strategy would express the reuse more simply.