Creational Patterns
Structural Patterns
Behavioral Patterns
The Flyweight Pattern is a structural design pattern that minimizes memory usage by sharing as much data as possible between similar objects. Imagine a text editor rendering a document with thousands of characters. Each character needs a font, size, and color — but most characters share the same formatting. Instead of storing duplicate formatting data in every character object, the Flyweight Pattern extracts the shared state into reusable objects.
This pattern is critical in performance-sensitive applications where creating millions of similar objects would consume too much memory.
You are building a text editor that represents every character in a document as an object. Each character object stores its value, font family, font size, color, and position. A 100,000-character document creates 100,000 objects, most of which share the same font, size, and color. The application consumes gigabytes of memory for data that is largely duplicated.
Simply put, the naive approach stores redundant data thousands of times over, wasting memory that could be avoided.
Split the object’s data into two categories: intrinsic state (shared, immutable data like font and color) and extrinsic state (unique, context-specific data like position). Create flyweight objects that store only the intrinsic state and share them across all characters that have the same formatting. Pass the extrinsic state (position) as method parameters when needed.
A flyweight factory ensures that only one flyweight object exists per unique combination of intrinsic state.
The Flyweight Pattern applies the principle of sharing to support large numbers of fine-grained objects efficiently. It trades a small increase in code complexity for a massive reduction in memory consumption. The key distinction is between intrinsic state (shareable) and extrinsic state (unique per context).
Here we build a character rendering system where formatting is shared across characters.
import java.util.HashMap;
import java.util.Map;
// Flyweight: stores shared intrinsic state (font properties)
public class CharacterStyle {
private final String fontFamily;
private final int fontSize;
private final String color;
public CharacterStyle(String fontFamily, int fontSize, String color) {
this.fontFamily = fontFamily;
this.fontSize = fontSize;
this.color = color;
}
public void render(char character, int row, int col) {
System.out.printf("Rendering '%c' at (%d,%d) with %s %dpx %s%n",
character, row, col, fontFamily, fontSize, color);
}
public String getKey() {
return fontFamily + "_" + fontSize + "_" + color;
}
}
// Flyweight Factory: ensures shared instances
public class StyleFactory {
private final Map<String, CharacterStyle> styles = new HashMap<>();
public CharacterStyle getStyle(String fontFamily, int fontSize, String color) {
String key = fontFamily + "_" + fontSize + "_" + color;
if (!styles.containsKey(key)) {
System.out.println("[Factory] Creating new style: " + key);
styles.put(key, new CharacterStyle(fontFamily, fontSize, color));
}
return styles.get(key);
}
public int getStyleCount() {
return styles.size();
}
}
// Context: stores extrinsic state (position) + reference to flyweight
public class Character {
private final char value;
private final int row;
private final int col;
private final CharacterStyle style; // Shared flyweight
public Character(char value, int row, int col, CharacterStyle style) {
this.value = value;
this.row = row;
this.col = col;
this.style = style;
}
public void render() {
style.render(value, row, col);
}
}
// Usage
public class Main {
public static void main(String[] args) {
StyleFactory factory = new StyleFactory();
// Simulate a document with many characters sharing few styles
List<Character> document = new ArrayList<>();
// Body text: thousands of characters, all same style
String bodyText = "The Flyweight Pattern saves memory by sharing objects.";
CharacterStyle bodyStyle = factory.getStyle("Arial", 12, "#333333");
for (int i = 0; i < bodyText.length(); i++) {
document.add(new Character(bodyText.charAt(i), 0, i, bodyStyle));
}
// Heading: different style, shared among heading characters
String heading = "Design Patterns";
CharacterStyle headingStyle = factory.getStyle("Arial", 24, "#000000");
for (int i = 0; i < heading.length(); i++) {
document.add(new Character(heading.charAt(i), 1, i, headingStyle));
}
// Code snippet: yet another shared style
String code = "Map<String, Object> cache";
CharacterStyle codeStyle = factory.getStyle("Courier", 14, "#008000");
for (int i = 0; i < code.length(); i++) {
document.add(new Character(code.charAt(i), 2, i, codeStyle));
}
// Render first few characters
for (int i = 0; i < 5; i++) {
document.get(i).render();
}
System.out.println("\nTotal characters: " + document.size());
System.out.println("Unique styles (flyweights): " + factory.getStyleCount());
System.out.println("Memory saved by sharing styles instead of duplicating them");
}
}
The same character rendering flyweight scenario in Python.
from dataclasses import dataclass
# Flyweight: stores shared intrinsic state (font properties)
class CharacterStyle:
def __init__(self, font_family: str, font_size: int, color: str):
self.font_family = font_family
self.font_size = font_size
self.color = color
def render(self, character: str, row: int, col: int) -> None:
print(f"Rendering '{character}' at ({row},{col}) "
f"with {self.font_family} {self.font_size}px {self.color}")
@property
def key(self) -> str:
return f"{self.font_family}_{self.font_size}_{self.color}"
# Flyweight Factory: ensures shared instances
class StyleFactory:
def __init__(self):
self._styles: dict[str, CharacterStyle] = {}
def get_style(self, font_family: str, font_size: int, color: str) -> CharacterStyle:
key = f"{font_family}_{font_size}_{color}"
if key not in self._styles:
print(f"[Factory] Creating new style: {key}")
self._styles[key] = CharacterStyle(font_family, font_size, color)
return self._styles[key]
@property
def style_count(self) -> int:
return len(self._styles)
# Context: stores extrinsic state (position) + reference to flyweight
@dataclass
class Character:
value: str
row: int
col: int
style: CharacterStyle # Shared flyweight
def render(self) -> None:
self.style.render(self.value, self.row, self.col)
# Usage
if __name__ == "__main__":
factory = StyleFactory()
document: list[Character] = []
# Body text: many characters, all same style
body_text = "The Flyweight Pattern saves memory by sharing objects."
body_style = factory.get_style("Arial", 12, "#333333")
for i, ch in enumerate(body_text):
document.append(Character(ch, row=0, col=i, style=body_style))
# Heading: different style, shared among heading characters
heading = "Design Patterns"
heading_style = factory.get_style("Arial", 24, "#000000")
for i, ch in enumerate(heading):
document.append(Character(ch, row=1, col=i, style=heading_style))
# Code snippet: yet another shared style
code = "cache: dict[str, object]"
code_style = factory.get_style("Courier", 14, "#008000")
for i, ch in enumerate(code):
document.append(Character(ch, row=2, col=i, style=code_style))
# Render first few characters
for char in document[:5]:
char.render()
print(f"\nTotal characters: {len(document)}")
print(f"Unique styles (flyweights): {factory.style_count}")
print("Memory saved by sharing styles instead of duplicating them")
String.intern() — The JVM’s string pool is a flyweight implementation, sharing identical string instancesInteger.valueOf() — Caches Integer objects for values -128 to 127, returning shared instancesa = 100; b = 100; a is b return TrueThe Composite Pattern is a structural design pattern that lets you compose objects into tree structures and then work with those structures as if they were individual objects. Think of a file system — a directory can contain files and other directories. Whether you ask for the size of a single file or an entire directory tree, you use the same operation. The composite handles the recursion for you.
This pattern is essential whenever you deal with hierarchical or tree-like structures where individual items and groups of items should be treated uniformly.
You are building a file system utility that calculates the total size of files and directories. A file has a fixed size, but a directory contains other files and subdirectories, each of which may contain more files and subdirectories. You need to calculate the total size at any level of the tree.
Without a unified interface, client code must constantly check whether it is dealing with a file or a directory, leading to scattered conditional logic and code that breaks every time a new type of node is added.
Define a common interface (FileSystemComponent) with a getSize() method. Files implement it by returning their own size. Directories implement it by summing the sizes of all their children — which can be files or other directories. Client code simply calls getSize() on any component without caring about its type.
The recursive structure is handled naturally by the composite, and the client code remains clean and simple.
The Composite Pattern follows the Uniform Treatment Principle — clients should not need to distinguish between individual objects and compositions of objects. It also leverages recursive composition, letting you build complex structures from simple, uniform building blocks.
Here we model a file system where files and directories share a common interface.
import java.util.ArrayList;
import java.util.List;
// Component interface
public interface FileSystemComponent {
String getName();
long getSize();
void display(String indent);
}
// Leaf: individual file
public class FileItem implements FileSystemComponent {
private final String name;
private final long size;
public FileItem(String name, long size) {
this.name = name;
this.size = size;
}
@Override
public String getName() { return name; }
@Override
public long getSize() { return size; }
@Override
public void display(String indent) {
System.out.printf("%s📄 %s (%d bytes)%n", indent, name, size);
}
}
// Composite: directory containing files and subdirectories
public class Directory implements FileSystemComponent {
private final String name;
private final List<FileSystemComponent> children = new ArrayList<>();
public Directory(String name) {
this.name = name;
}
public Directory add(FileSystemComponent component) {
children.add(component);
return this; // Fluent API
}
public void remove(FileSystemComponent component) {
children.remove(component);
}
@Override
public String getName() { return name; }
@Override
public long getSize() {
// Recursively sums sizes of all children
return children.stream()
.mapToLong(FileSystemComponent::getSize)
.sum();
}
@Override
public void display(String indent) {
System.out.printf("%s📁 %s/ (%d bytes total)%n", indent, name, getSize());
for (FileSystemComponent child : children) {
child.display(indent + " ");
}
}
}
// Usage
public class Main {
public static void main(String[] args) {
// Build a file system tree
Directory root = new Directory("project");
Directory src = new Directory("src");
src.add(new FileItem("Main.java", 2400));
src.add(new FileItem("Utils.java", 1200));
Directory config = new Directory("config");
config.add(new FileItem("application.yml", 350));
config.add(new FileItem("logback.xml", 500));
src.add(config);
Directory test = new Directory("test");
test.add(new FileItem("MainTest.java", 1800));
root.add(src);
root.add(test);
root.add(new FileItem("README.md", 600));
root.add(new FileItem("pom.xml", 3200));
// Uniform interface - works the same for files and directories
root.display("");
System.out.println("\nTotal project size: " + root.getSize() + " bytes");
// Works on any subtree too
System.out.println("Source directory size: " + src.getSize() + " bytes");
}
}
The same file system composite scenario in Python.
from abc import ABC, abstractmethod
# Component interface
class FileSystemComponent(ABC):
@abstractmethod
def get_name(self) -> str:
pass
@abstractmethod
def get_size(self) -> int:
pass
@abstractmethod
def display(self, indent: str = "") -> None:
pass
# Leaf: individual file
class FileItem(FileSystemComponent):
def __init__(self, name: str, size: int):
self._name = name
self._size = size
def get_name(self) -> str:
return self._name
def get_size(self) -> int:
return self._size
def display(self, indent: str = "") -> None:
print(f"{indent}file: {self._name} ({self._size} bytes)")
# Composite: directory containing files and subdirectories
class Directory(FileSystemComponent):
def __init__(self, name: str):
self._name = name
self._children: list[FileSystemComponent] = []
def add(self, component: FileSystemComponent) -> "Directory":
self._children.append(component)
return self # Fluent API
def remove(self, component: FileSystemComponent) -> None:
self._children.remove(component)
def get_name(self) -> str:
return self._name
def get_size(self) -> int:
# Recursively sums sizes of all children
return sum(child.get_size() for child in self._children)
def display(self, indent: str = "") -> None:
print(f"{indent}dir: {self._name}/ ({self.get_size()} bytes total)")
for child in self._children:
child.display(indent + " ")
# Usage
if __name__ == "__main__":
# Build a file system tree
root = Directory("project")
src = Directory("src")
src.add(FileItem("Main.java", 2400))
src.add(FileItem("Utils.java", 1200))
config = Directory("config")
config.add(FileItem("application.yml", 350))
config.add(FileItem("logback.xml", 500))
src.add(config)
test = Directory("test")
test.add(FileItem("MainTest.java", 1800))
root.add(src)
root.add(test)
root.add(FileItem("README.md", 600))
root.add(FileItem("pom.xml", 3200))
# Uniform interface - works the same for files and directories
root.display()
print(f"\nTotal project size: {root.get_size()} bytes")
# Works on any subtree too
print(f"Source directory size: {src.get_size()} bytes")
Component and Container form a composite tree where containers hold other componentsjava.io.File — Represents both files and directories with shared operations like length() and listFiles()ast module — Abstract Syntax Trees where nodes contain other nodes uniformlyGrantedAuthority hierarchies where roles can contain other rolesThe Iterator Pattern provides a way to access elements of a collection sequentially without exposing its underlying representation. Whether the data lives in an array, a linked list, a tree, or a paginated API, the iterator gives clients a uniform interface to traverse it: hasNext() and next().
This pattern separates the traversal logic from the collection itself, allowing multiple traversal strategies over the same data structure without modifying the collection class.
You are building a service that fetches results from a paginated REST API. The API returns 20 items per page, and you need to process all items across all pages. The naive approach is to embed the pagination logic directly in your business code — tracking page numbers, checking for the last page, and concatenating results.
Now imagine another part of your codebase also needs to iterate over the same API but with different filtering. You end up duplicating pagination logic everywhere. Worse, if the API changes its pagination scheme (from page-based to cursor-based), you must update every call site.
The Iterator Pattern encapsulates the pagination logic inside an iterator object. Clients simply call hasNext() and next() without knowing whether the data comes from one page or fifty. The iterator handles fetching the next page transparently.
This means your business code stays clean and focused on processing items, while the iterator handles the mechanics of traversal and data fetching.
Single Responsibility Principle. The collection is responsible for storing data. The iterator is responsible for traversing it. Neither takes on the other’s job, making both easier to maintain and extend.
Scenario: An iterator that transparently traverses paginated API results.
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;
// Simulated API response
public class Page<T> {
private final List<T> items;
private final int currentPage;
private final int totalPages;
public Page(List<T> items, int currentPage, int totalPages) {
this.items = items;
this.currentPage = currentPage;
this.totalPages = totalPages;
}
public List<T> getItems() { return items; }
public int getCurrentPage() { return currentPage; }
public int getTotalPages() { return totalPages; }
public boolean hasNextPage() { return currentPage < totalPages; }
}
// Simulated API client
public class ProductApiClient {
private final List<List<String>> pages = List.of(
List.of("Laptop", "Keyboard", "Mouse"),
List.of("Monitor", "Headphones", "Webcam"),
List.of("USB Hub", "Desk Lamp")
);
public Page<String> fetchPage(int pageNumber) {
System.out.println(" [API] Fetching page " + pageNumber + "...");
int index = pageNumber - 1;
return new Page<>(pages.get(index), pageNumber, pages.size());
}
}
// Iterator for paginated results
public class PaginatedIterator<T> implements Iterator<T> {
private final ProductApiClient apiClient;
private Page<String> currentPage;
private int itemIndex = 0;
public PaginatedIterator(ProductApiClient apiClient) {
this.apiClient = apiClient;
this.currentPage = apiClient.fetchPage(1);
}
@Override
public boolean hasNext() {
if (itemIndex < currentPage.getItems().size()) {
return true;
}
// Current page exhausted — check if more pages exist
return currentPage.hasNextPage();
}
@Override
@SuppressWarnings("unchecked")
public T next() {
// If current page is exhausted, fetch the next one
if (itemIndex >= currentPage.getItems().size()) {
if (!currentPage.hasNextPage()) {
throw new NoSuchElementException("No more items");
}
currentPage = apiClient.fetchPage(
currentPage.getCurrentPage() + 1);
itemIndex = 0;
}
return (T) currentPage.getItems().get(itemIndex++);
}
}
// Iterable collection that provides the iterator
public class ProductCatalog implements Iterable<String> {
private final ProductApiClient apiClient;
public ProductCatalog(ProductApiClient apiClient) {
this.apiClient = apiClient;
}
@Override
public Iterator<String> iterator() {
return new PaginatedIterator<>(apiClient);
}
}
// Usage
public class Main {
public static void main(String[] args) {
ProductApiClient api = new ProductApiClient();
ProductCatalog catalog = new ProductCatalog(api);
System.out.println("Iterating over all products:\n");
// Clean for-each loop — pagination is invisible
int count = 1;
for (String product : catalog) {
System.out.println(" " + count++ + ". " + product);
}
System.out.println("\nProcessed all products across all pages.");
}
}
Same paginated API traversal in Python using the iterator protocol.
from dataclasses import dataclass
@dataclass
class Page:
items: list[str]
current_page: int
total_pages: int
@property
def has_next_page(self) -> bool:
return self.current_page < self.total_pages
# Simulated API client
class ProductApiClient:
def __init__(self):
self._pages = [
["Laptop", "Keyboard", "Mouse"],
["Monitor", "Headphones", "Webcam"],
["USB Hub", "Desk Lamp"],
]
def fetch_page(self, page_number: int) -> Page:
print(f" [API] Fetching page {page_number}...")
items = self._pages[page_number - 1]
return Page(items, page_number, len(self._pages))
# Iterator for paginated results
class PaginatedIterator:
def __init__(self, api_client: ProductApiClient):
self._api_client = api_client
self._current_page = api_client.fetch_page(1)
self._item_index = 0
def __iter__(self):
return self
def __next__(self) -> str:
# If current page is exhausted, fetch the next one
if self._item_index >= len(self._current_page.items):
if not self._current_page.has_next_page:
raise StopIteration
next_page_num = self._current_page.current_page + 1
self._current_page = self._api_client.fetch_page(next_page_num)
self._item_index = 0
item = self._current_page.items[self._item_index]
self._item_index += 1
return item
# Iterable collection that provides the iterator
class ProductCatalog:
def __init__(self, api_client: ProductApiClient):
self._api_client = api_client
def __iter__(self) -> PaginatedIterator:
return PaginatedIterator(self._api_client)
# Usage
if __name__ == "__main__":
api = ProductApiClient()
catalog = ProductCatalog(api)
print("Iterating over all products:\n")
# Clean for loop — pagination is invisible
for count, product in enumerate(catalog, start=1):
print(f" {count}. {product}")
print("\nProcessed all products across all pages.")
Iterator and Iterable — The foundation of Java’s for-each loop. Every collection in java.util implements Iterable.for loop uses the iterator protocol (__iter__ / __next__). Generators with yield are a concise way to create iterators.ResultSet — Iterates over database query results row by row without loading the entire result set into memory.Page / Slice — Spring Data’s pagination abstractions use the iterator pattern to traverse large datasets from repositories.The State Pattern allows an object to alter its behavior when its internal state changes, making it appear as though the object has changed its class. Instead of scattering state-dependent logic across methods with conditionals, you encapsulate each state into its own class, and the object delegates behavior to its current state.
If you have ever seen a method riddled with if (status == "pending") checks scattered throughout a class, the State Pattern is the clean alternative.
You are building an order management system. An order moves through several states: Pending, Processing, Shipped, and Delivered. Each state allows different actions — you can cancel a pending order but not a shipped one. You can ship a processing order but not a delivered one.
The naive approach is to add status checks in every method: cancel() checks if status is “pending,” ship() checks if status is “processing,” and so on. As states and transitions grow, the class becomes a tangled web of conditionals. Adding a new state (like “Returned”) means touching every method.
The State Pattern creates a separate class for each state. Each state class implements the same interface and defines what actions are valid in that state. The order object (context) holds a reference to its current state and delegates all actions to it. State transitions happen by swapping the current state object.
This eliminates conditional logic and makes each state’s behavior self-contained. Adding a new state means adding a new class — no existing state classes need to change.
Encapsulate what varies. The thing that varies here is the behavior associated with each state. By extracting each state into its own class, you isolate state-specific logic and make transitions explicit and traceable.
Scenario: An order management system with state-driven behavior.
// State interface
public interface OrderState {
void next(OrderContext order);
void cancel(OrderContext order);
String getStatus();
}
// Context
public class OrderContext {
private OrderState state;
private final String orderId;
public OrderContext(String orderId) {
this.orderId = orderId;
this.state = new PendingState();
System.out.println("Order " + orderId + " created. Status: " + state.getStatus());
}
public void setState(OrderState state) {
this.state = state;
System.out.println("Order " + orderId + " status: " + state.getStatus());
}
public void next() {
state.next(this);
}
public void cancel() {
state.cancel(this);
}
public String getStatus() {
return state.getStatus();
}
public String getOrderId() {
return orderId;
}
}
// Concrete State: Pending
public class PendingState implements OrderState {
@Override
public void next(OrderContext order) {
System.out.println("Processing payment for order " + order.getOrderId() + "...");
order.setState(new ProcessingState());
}
@Override
public void cancel(OrderContext order) {
System.out.println("Order " + order.getOrderId() + " cancelled.");
order.setState(new CancelledState());
}
@Override
public String getStatus() {
return "PENDING";
}
}
// Concrete State: Processing
public class ProcessingState implements OrderState {
@Override
public void next(OrderContext order) {
System.out.println("Order " + order.getOrderId() + " shipped!");
order.setState(new ShippedState());
}
@Override
public void cancel(OrderContext order) {
System.out.println("Order " + order.getOrderId()
+ " is being processed. Initiating cancellation...");
order.setState(new CancelledState());
}
@Override
public String getStatus() {
return "PROCESSING";
}
}
// Concrete State: Shipped
public class ShippedState implements OrderState {
@Override
public void next(OrderContext order) {
System.out.println("Order " + order.getOrderId() + " delivered.");
order.setState(new DeliveredState());
}
@Override
public void cancel(OrderContext order) {
System.out.println("Cannot cancel order " + order.getOrderId()
+ " — already shipped.");
}
@Override
public String getStatus() {
return "SHIPPED";
}
}
// Concrete State: Delivered
public class DeliveredState implements OrderState {
@Override
public void next(OrderContext order) {
System.out.println("Order " + order.getOrderId()
+ " already delivered. No further transitions.");
}
@Override
public void cancel(OrderContext order) {
System.out.println("Cannot cancel order " + order.getOrderId()
+ " — already delivered.");
}
@Override
public String getStatus() {
return "DELIVERED";
}
}
// Concrete State: Cancelled
public class CancelledState implements OrderState {
@Override
public void next(OrderContext order) {
System.out.println("Order " + order.getOrderId()
+ " is cancelled. Cannot proceed.");
}
@Override
public void cancel(OrderContext order) {
System.out.println("Order " + order.getOrderId()
+ " is already cancelled.");
}
@Override
public String getStatus() {
return "CANCELLED";
}
}
// Usage
public class Main {
public static void main(String[] args) {
OrderContext order = new OrderContext("ORD-2001");
order.next(); // Pending -> Processing
order.next(); // Processing -> Shipped
order.cancel(); // Cannot cancel — already shipped
order.next(); // Shipped -> Delivered
order.next(); // Already delivered
System.out.println("\n--- Cancellation flow ---");
OrderContext order2 = new OrderContext("ORD-2002");
order2.cancel(); // Pending -> Cancelled
order2.next(); // Cannot proceed — cancelled
}
}
Same order management system in Python.
from abc import ABC, abstractmethod
# State interface
class OrderState(ABC):
@abstractmethod
def next(self, order: "OrderContext") -> None:
pass
@abstractmethod
def cancel(self, order: "OrderContext") -> None:
pass
@abstractmethod
def get_status(self) -> str:
pass
# Context
class OrderContext:
def __init__(self, order_id: str):
self.order_id = order_id
self._state: OrderState = PendingState()
print(f"Order {order_id} created. Status: {self._state.get_status()}")
def set_state(self, state: OrderState) -> None:
self._state = state
print(f"Order {self.order_id} status: {self._state.get_status()}")
def next(self) -> None:
self._state.next(self)
def cancel(self) -> None:
self._state.cancel(self)
@property
def status(self) -> str:
return self._state.get_status()
# Concrete State: Pending
class PendingState(OrderState):
def next(self, order: OrderContext) -> None:
print(f"Processing payment for order {order.order_id}...")
order.set_state(ProcessingState())
def cancel(self, order: OrderContext) -> None:
print(f"Order {order.order_id} cancelled.")
order.set_state(CancelledState())
def get_status(self) -> str:
return "PENDING"
# Concrete State: Processing
class ProcessingState(OrderState):
def next(self, order: OrderContext) -> None:
print(f"Order {order.order_id} shipped!")
order.set_state(ShippedState())
def cancel(self, order: OrderContext) -> None:
print(f"Order {order.order_id} is being processed. "
f"Initiating cancellation...")
order.set_state(CancelledState())
def get_status(self) -> str:
return "PROCESSING"
# Concrete State: Shipped
class ShippedState(OrderState):
def next(self, order: OrderContext) -> None:
print(f"Order {order.order_id} delivered.")
order.set_state(DeliveredState())
def cancel(self, order: OrderContext) -> None:
print(f"Cannot cancel order {order.order_id} — already shipped.")
def get_status(self) -> str:
return "SHIPPED"
# Concrete State: Delivered
class DeliveredState(OrderState):
def next(self, order: OrderContext) -> None:
print(f"Order {order.order_id} already delivered. "
f"No further transitions.")
def cancel(self, order: OrderContext) -> None:
print(f"Cannot cancel order {order.order_id} — already delivered.")
def get_status(self) -> str:
return "DELIVERED"
# Concrete State: Cancelled
class CancelledState(OrderState):
def next(self, order: OrderContext) -> None:
print(f"Order {order.order_id} is cancelled. Cannot proceed.")
def cancel(self, order: OrderContext) -> None:
print(f"Order {order.order_id} is already cancelled.")
def get_status(self) -> str:
return "CANCELLED"
# Usage
if __name__ == "__main__":
order = OrderContext("ORD-2001")
order.next() # Pending -> Processing
order.next() # Processing -> Shipped
order.cancel() # Cannot cancel — already shipped
order.next() # Shipped -> Delivered
order.next() # Already delivered
print("\n--- Cancellation flow ---")
order2 = OrderContext("ORD-2002")
order2.cancel() # Pending -> Cancelled
order2.next() # Cannot proceed — cancelled
if/else or switch statements that check the current state before acting.Thread class has states (NEW, RUNNABLE, BLOCKED, TERMINATED) that determine allowed operations.spring-statemachine project provides a framework for building state-based applications using this pattern.The Template Method Pattern defines the skeleton of an algorithm in a base class, letting subclasses override specific steps without changing the algorithm’s overall structure. The base class controls the workflow — the “what” and “when” — while subclasses provide the “how” for individual steps.
This pattern is everywhere in frameworks. Every time you extend a base class and override a hook method (like setUp() in a test framework or doGet() in a servlet), you are using the Template Method Pattern.
You are building a data export system. Users can export records as CSV, JSON, or XML. Each export follows the same high-level workflow: fetch data, validate it, transform it into the target format, and write the output. The naive approach is to implement three separate export classes, each duplicating the fetch and validation logic.
This leads to code duplication. When the validation rules change, you must update three classes. When you add a new export format, you copy-paste an existing class and modify the format-specific parts — a recipe for bugs and inconsistency.
The Template Method Pattern pulls the shared workflow into a base class method (the “template method”) that calls abstract steps for the parts that vary. The base class handles fetching and validation, while subclasses implement only the format-specific transformation and writing logic.
The template method is typically marked as final (in Java) to prevent subclasses from altering the workflow order. Subclasses can customize individual steps but cannot skip validation or reorder the pipeline.
The Hollywood Principle: “Don’t call us, we’ll call you.” The base class controls the flow and calls subclass methods at the right time. Subclasses do not drive the algorithm — they fill in the blanks.
Scenario: A data export pipeline supporting CSV, JSON, and XML formats.
import java.util.List;
import java.util.Map;
// Abstract base class with template method
public abstract class DataExporter {
// Template method — defines the algorithm skeleton
public final void export(List<Map<String, String>> records) {
validate(records);
String formatted = transform(records);
String fileName = getFileName();
writeOutput(fileName, formatted);
System.out.println("Export complete: " + fileName + "\n");
}
// Shared step — same for all formats
private void validate(List<Map<String, String>> records) {
if (records == null || records.isEmpty()) {
throw new IllegalArgumentException("No records to export");
}
System.out.println("Validated " + records.size() + " records.");
}
// Abstract steps — subclasses provide the implementation
protected abstract String transform(List<Map<String, String>> records);
protected abstract String getFileName();
// Hook method — default implementation, can be overridden
protected void writeOutput(String fileName, String content) {
System.out.println("Writing to " + fileName
+ " (" + content.length() + " chars)");
}
}
// Concrete: CSV Exporter
public class CsvExporter extends DataExporter {
@Override
protected String transform(List<Map<String, String>> records) {
StringBuilder sb = new StringBuilder();
// Header row
sb.append(String.join(",", records.get(0).keySet())).append("\n");
// Data rows
for (Map<String, String> record : records) {
sb.append(String.join(",", record.values())).append("\n");
}
return sb.toString();
}
@Override
protected String getFileName() {
return "export.csv";
}
}
// Concrete: JSON Exporter
public class JsonExporter extends DataExporter {
@Override
protected String transform(List<Map<String, String>> records) {
StringBuilder sb = new StringBuilder("[\n");
for (int i = 0; i < records.size(); i++) {
sb.append(" {");
Map<String, String> record = records.get(i);
int j = 0;
for (Map.Entry<String, String> entry : record.entrySet()) {
sb.append("\"").append(entry.getKey()).append("\": \"")
.append(entry.getValue()).append("\"");
if (++j < record.size()) sb.append(", ");
}
sb.append("}");
if (i < records.size() - 1) sb.append(",");
sb.append("\n");
}
sb.append("]");
return sb.toString();
}
@Override
protected String getFileName() {
return "export.json";
}
}
// Concrete: XML Exporter
public class XmlExporter extends DataExporter {
@Override
protected String transform(List<Map<String, String>> records) {
StringBuilder sb = new StringBuilder("<records>\n");
for (Map<String, String> record : records) {
sb.append(" <record>\n");
for (Map.Entry<String, String> entry : record.entrySet()) {
sb.append(" <").append(entry.getKey()).append(">")
.append(entry.getValue())
.append("</").append(entry.getKey()).append(">\n");
}
sb.append(" </record>\n");
}
sb.append("</records>");
return sb.toString();
}
@Override
protected String getFileName() {
return "export.xml";
}
}
// Usage
public class Main {
public static void main(String[] args) {
List<Map<String, String>> records = List.of(
Map.of("name", "Alice", "role", "Engineer"),
Map.of("name", "Bob", "role", "Designer")
);
new CsvExporter().export(records);
new JsonExporter().export(records);
new XmlExporter().export(records);
}
}
Same data export pipeline in Python.
from abc import ABC, abstractmethod
class DataExporter(ABC):
"""Abstract base class with template method."""
def export(self, records: list[dict[str, str]]) -> None:
"""Template method — defines the algorithm skeleton."""
self._validate(records)
formatted = self.transform(records)
file_name = self.get_file_name()
self.write_output(file_name, formatted)
print(f"Export complete: {file_name}\n")
def _validate(self, records: list[dict[str, str]]) -> None:
"""Shared step — same for all formats."""
if not records:
raise ValueError("No records to export")
print(f"Validated {len(records)} records.")
@abstractmethod
def transform(self, records: list[dict[str, str]]) -> str:
"""Subclasses provide format-specific transformation."""
pass
@abstractmethod
def get_file_name(self) -> str:
pass
def write_output(self, file_name: str, content: str) -> None:
"""Hook method — default implementation, can be overridden."""
print(f"Writing to {file_name} ({len(content)} chars)")
class CsvExporter(DataExporter):
def transform(self, records: list[dict[str, str]]) -> str:
headers = ",".join(records[0].keys())
rows = [",".join(record.values()) for record in records]
return headers + "\n" + "\n".join(rows)
def get_file_name(self) -> str:
return "export.csv"
class JsonExporter(DataExporter):
def transform(self, records: list[dict[str, str]]) -> str:
import json
return json.dumps(records, indent=2)
def get_file_name(self) -> str:
return "export.json"
class XmlExporter(DataExporter):
def transform(self, records: list[dict[str, str]]) -> str:
lines = ["<records>"]
for record in records:
lines.append(" <record>")
for key, value in record.items():
lines.append(f" <{key}>{value}</{key}>")
lines.append(" </record>")
lines.append("</records>")
return "\n".join(lines)
def get_file_name(self) -> str:
return "export.xml"
# Usage
if __name__ == "__main__":
records = [
{"name": "Alice", "role": "Engineer"},
{"name": "Bob", "role": "Designer"},
]
CsvExporter().export(records)
JsonExporter().export(records)
XmlExporter().export(records)
HttpServlet — The service() method is the template that dispatches to doGet(), doPost(), etc.setUp(), test method, tearDown()) is a template method pattern.JdbcTemplate — Handles connection management and error handling while letting you provide the query-specific logic.unittest.TestCase — The setUp() / test_*() / tearDown() lifecycle follows the template method pattern.