Template Method Pattern
Define the skeleton of an algorithm, deferring some steps to subclasses (or in FP: to injected functions).
The Problemβ
You're building a resume generator. Every resume follows the same structure: Header β Summary β Experience β Skills β Education. But a developer's resume highlights TypeScript and system design, a designer's highlights Figma and design systems, and a manager's highlights team building and OKRs.
The naive approach:
function buildDeveloperResume() {
return [
devHeader(),
devSummary(),
devExperience(),
devSkills(),
devEducation(),
];
}
function buildDesignerResume() {
return [
designerHeader(),
designerSummary(),
designerExperience(),
designerSkills(),
designerEducation(),
];
}
// Same skeleton repeated for every profile
The structure is identical. Only the content of each step changes. Adding a new section means updating every function.
The Solutionβ
Define the skeleton once with default steps. Each profile only overrides what's different:
import { templateWithDefaults } from "@pithos/core/eidos/template/template";
const buildResume = templateWithDefaults(
(steps: ResumeSteps) =>
(): ResumeSection[] => [
steps.header(),
steps.summary(),
steps.experience(),
steps.skills(),
steps.education(),
],
DEFAULT_STEPS, // base implementation for all 5 steps
);
// Developer: override 3 steps, keep default header + education
const devResume = buildResume({
summary: () => ({ title: "Summary", content: ["Full-stack developer with 7+ years..."] }),
experience: () => ({ title: "Experience", content: ["Senior Engineer β Stripe..."] }),
skills: () => ({ title: "Skills", content: ["TypeScript Β· React Β· Node.js..."] }),
});
// Designer: override all 5 steps (different portfolio link, different education)
const designerResume = buildResume({
header: () => ({ title: "Alex Johnson", content: ["...", "portfolio.alexj.design"] }),
summary: () => ({ title: "Summary", content: ["Product designer with 6+ years..."] }),
experience: () => ({ title: "Experience", content: ["Senior Designer β Figma..."] }),
skills: () => ({ title: "Skills", content: ["Figma Β· Design Systems..."] }),
education: () => ({ title: "Education", content: ["M.F.A. Interaction Design β RISD"] }),
});
The skeleton (Header β Summary β Experience β Skills β Education) is defined once. Each profile only specifies what's different. templateWithDefaults merges the overrides with the defaults automatically.
Live Demoβ
Switch between Developer, Designer, and Manager profiles. The template skeleton is always the same 5 steps in the same order. The step pipeline on the left shows which steps are overridden (amber) vs using defaults (gray). When you switch profiles, the steps execute sequentially to show the template in action.
- template.ts
- Usage
/**
* Resume builder using the Template Method pattern.
*
* The skeleton is fixed: Header β Summary β Experience β Skills β Education.
* Each profile overrides specific steps via `templateWithDefaults`.
*/
import { templateWithDefaults } from "@pithos/core/eidos/template/template";
import { DEFAULT_STEPS } from "@/data/profiles";
import { PROFILE_OVERRIDES } from "@/data/overrides";
import type { ProfileKey, ResumeData, ResumeSection, ResumeSteps } from "./types";
/** The skeleton β step order never changes */
const buildResume = templateWithDefaults(
(steps: ResumeSteps) =>
(): ResumeSection[] => [
steps.header(),
steps.summary(),
steps.experience(),
steps.hardSkills(),
steps.softSkills(),
steps.education(),
],
DEFAULT_STEPS,
);
/** Get the list of step keys that a profile overrides */
export function getOverrides(profile: ProfileKey): (keyof ResumeSteps)[] {
return Object.keys(PROFILE_OVERRIDES[profile]) as (keyof ResumeSteps)[];
}
/** Generate a resume for a given profile */
export function generateResume(profile: ProfileKey): ResumeData {
const overrides = PROFILE_OVERRIDES[profile];
const generator = buildResume(overrides);
return {
sections: generator(),
overrides: getOverrides(profile),
};
}
import { useState, useMemo, useEffect, useRef, useCallback } from "react";
import { generateResume } from "@/lib/template";
import { PROFILES, STEP_ORDER } from "@/data/profiles";
import type { ProfileKey } from "@/lib/types";
export function useResumeBuilder() {
const [profile, setProfile] = useState<ProfileKey>("developer");
const [executingStep, setExecutingStep] = useState<number | null>(null);
const [completedSteps, setCompletedSteps] = useState<Set<number>>(new Set());
const [animating, setAnimating] = useState(false);
const abortRef = useRef<AbortController | null>(null);
const resume = useMemo(() => generateResume(profile), [profile]);
const profileMeta = PROFILES.find((p) => p.key === profile);
const runAnimation = useCallback((key: ProfileKey) => {
abortRef.current?.abort();
const ctrl = new AbortController();
abortRef.current = ctrl;
setProfile(key);
setAnimating(true);
setCompletedSteps(new Set());
setExecutingStep(null);
const wait = (ms: number) => new Promise<void>((r) => setTimeout(r, ms));
(async () => {
await wait(500);
if (ctrl.signal.aborted) return;
setExecutingStep(0);
for (let i = 0; i < STEP_ORDER.length; i++) {
await wait(400);
if (ctrl.signal.aborted) return;
setCompletedSteps((prev) => new Set([...prev, i]));
if (i < STEP_ORDER.length - 1) {
setExecutingStep(i + 1);
} else {
setExecutingStep(null);
setAnimating(false);
}
}
})();
}, []);
useEffect(() => {
runAnimation("developer");
return () => abortRef.current?.abort();
}, [runAnimation]);
return {
profile,
resume,
profileMeta,
executingStep,
completedSteps,
animating,
runAnimation,
};
}
Real-World Analogyβ
A recipe template. "Make soup: 1) prep ingredients, 2) boil water, 3) add ingredients, 4) simmer, 5) serve." The template is the same for all soups. Each soup recipe just fills in what ingredients to prep and add.
When to Use Itβ
- Multiple algorithms share the same structure
- You want to enforce a sequence of steps
- Some steps vary while others stay constant
- You need sensible defaults with selective overrides
When NOT to Use Itβ
If every step is different for every variant, there's no shared skeleton. You just have different functions. Template Method shines when the structure is fixed and only the content varies.
APIβ
- templateWithDefaults β Create algorithm templates with overridable steps