Module 25 - Design Patterns
Design patterns are reusable solutions to recurring software design problems. This module covers all three GoF categories - creational, structural, and behavioural - with idiomatic Java 21 implementations and practical commentary.
Creational Patterns
Creational patterns abstract the object-creation process, decoupling callers from the concrete classes they need.
Singleton - init-on-demand holder
public static class AppConfig {
private AppConfig() { /* load defaults */ }
private static class Holder {
static final AppConfig INSTANCE = new AppConfig();
}
public static AppConfig getInstance() { return Holder.INSTANCE; }
}
Thread-safe lazy initialisation without synchronized. The inner Holder class is not loaded until getInstance() is first called; the JVM class-loading guarantee provides atomicity for free.
Factory Method
public abstract static class NotificationService {
protected abstract Notification createNotification(); // factory method
public void notify(String recipient, String message) {
createNotification().send(recipient, message); // uses the product
}
}
The creator declares the factory method; subclasses (EmailService, SmsService) decide which concrete product to instantiate. A static factory variant (notificationFor("EMAIL")) achieves the same result without subclassing.
Abstract Factory
public interface UIFactory {
Button createButton();
Checkbox createCheckbox();
}
// LightThemeFactory / DarkThemeFactory implement UIFactory
List<String> rendered = renderUI(new DarkThemeFactory());
Creates entire families of related objects without specifying concrete classes. Swap one factory to swap the entire look & feel.
Builder
HttpRequest req = HttpRequest.newBuilder("POST", "http://api.example.com")
.header("Content-Type", "application/json")
.body("{\"key\":\"value\"}")
.timeoutMs(10_000)
.followRedirects(false)
.build();
Solves the “telescoping constructor” anti-pattern. Required fields are constructor parameters of Builder; everything else is optional with sensible defaults. The result (HttpRequest) is immutable.
Prototype
DocumentTemplate copy = original.copy(); // deep copy
DocumentTemplate variant = original.withTitle("New Title");
Clones an existing object instead of re-creating it from scratch. copy() performs a deep copy of mutable collections so mutations in one instance do not affect the other.
Object Pool
ObjectPool<StringBuilder> pool = new ObjectPool<>(StringBuilder::new, 10);
StringBuilder sb = pool.acquire(); // borrow
// … use sb …
pool.release(sb); // return
Pre-creates expensive objects and reuses them. acquire() pulls from the available queue or creates up to maxSize; throws IllegalStateException if the pool is exhausted. Used in practice for DB connections, ByteBuffers, and thread pools.
Structural Patterns
Structural patterns compose objects and classes into larger structures while keeping those structures flexible and efficient.
Adapter
// Legacy library returns Fahrenheit; our interface expects Celsius
TemperatureSource adapted = new ThermometerAdapter(legacyThermometer);
double celsius = adapted.getCelsius();
// Lambda variant
TemperatureSource adapted = StructuralPatterns.adapt(legacyThermometer);
Wraps an incompatible interface without modifying the legacy class. The lambda variant works naturally when TemperatureSource is a functional interface.
Decorator
TextProcessor p = new UpperCaseDecorator(
new TrimDecorator(
new IdentityProcessor()));
p.process(" hello "); // → "HELLO"
Adds behaviour by wrapping the original object - each decorator adds one concern. Decorators compose freely in any order. Java I/O streams (FileInputStream → BufferedInputStream → GZIPInputStream) are the canonical real-world example.
Facade
VideoUploadFacade facade = new VideoUploadFacade();
UploadResult result = facade.upload("My Video", rawVideo, rawAudio);
Hides four subsystems (VideoEncoder, AudioNormaliser, ThumbnailGenerator, MetadataWriter) behind a single method call. Clients never need to know the subsystems exist.
Composite
DirectoryNode root = new DirectoryNode("project");
root.add(new FileNode("README.md", 1024));
root.add(new DirectoryNode("src").add(new FileNode("Main.java", 3000)));
long total = root.size(); // recurses through the whole tree
Lets clients treat leaf nodes (FileNode) and composite nodes (DirectoryNode) identically through the FileSystemNode interface.
Proxy
CachingImageProxy proxy = new CachingImageProxy();
proxy.load("hero.png"); // disk I/O
proxy.load("hero.png"); // served from cache
proxy.diskLoads(); // 1
Controls access to the real subject. The caching proxy (computeIfAbsent) ensures disk I/O happens at most once per path.
Bridge
Shape s1 = new BridgeCircle(new VectorRenderer(), 5.0);
Shape s2 = new BridgeCircle(new RasterRenderer(), 5.0);
Decouples the abstraction (Shape) from the implementation (Renderer) so both can vary independently. Avoids the combinatorial class explosion that inheritance would cause (2 shapes × 2 renderers = 4 subclasses; Bridge needs 2 + 2 = 4 classes total regardless of how many shapes or renderers are added later).
Flyweight
GlyphType type = GlyphFactory.get('A', "Arial", 12); // shared
RenderedGlyph rg = new RenderedGlyph(type, x, y, "black"); // unique per position
Shares intrinsic state (character, font, size) across thousands of rendered glyphs. Extrinsic state (position, colour) is passed at use time. Classic example: character glyphs in a text editor.
Behavioural Patterns
Behavioural patterns deal with algorithms and the assignment of responsibilities between objects.
Observer
EventBus<StockPrice> bus = new EventBus<>();
PriceTracker tracker = new PriceTracker();
bus.subscribe(tracker);
bus.publish(new StockPrice("AAPL", 150.0)); // tracker notified
bus.unsubscribe(tracker);
Subjects notify registered listeners without knowing who they are. Java equivalents: PropertyChangeListener, reactive Flow.Publisher.
Strategy
Sorter<Integer> sorter = new Sorter<>(new BubbleSortStrategy<>());
sorter.setStrategy(new JavaSortStrategy<>()); // swap at runtime
List<Integer> sorted = sorter.sort(items, Comparator.naturalOrder());
Encapsulates a family of algorithms and makes them interchangeable. The Sorter context delegates to whichever SortStrategy is currently configured. In modern Java, strategies are often plain lambdas.
Command
TextDocument doc = new TextDocument();
doc.execute(doc.appendCommand("Hello "));
doc.execute(doc.appendCommand("World"));
doc.undo(); // → "Hello "
doc.redo(); // → "Hello World"
Encapsulates a request as an object. Each Command pairs execute() with undo(). The TextDocument maintains a history stack and a redo stack.
Chain of Responsibility
RequestHandler pipeline = chain(
new AuthHandler("token123"),
new RateLimitHandler(100),
new LoggingHandler(),
new EchoHandler()
);
HttpResponse resp = pipeline.handle(request);
Passes a request along a chain until a handler processes it. Each handler either handles the request or calls passToNext(). Servlet filter chains and middleware pipelines use this pattern.
Template Method
ReportGenerator gen = new HtmlReport();
String report = gen.generate("Sales Q1", List.of("row1", "row2"));
// <h1>Sales Q1</h1> <ul> <li>row1</li> ... </ul>
Defines a skeleton algorithm (generate) in the base class; subclasses (PlainTextReport, CsvReport, HtmlReport) override the hook methods without changing the overall sequence.
Iterator
// Range iterator
for (int i : new IntRange(1, 10, 2)) { /* 1, 3, 5, 7, 9 */ }
// In-order BST iterator
BinaryTree<Integer> tree = new BinaryTree<>();
for (int v : List.of(5, 3, 7)) tree.insert(v);
for (int v : tree) { /* 3, 5, 7 */ }
Provides sequential access without exposing internal structure. The iterative BST iterator uses an explicit stack to avoid recursion.
State
Order order = new Order();
order.confirm(); // PENDING → CONFIRMED
order.ship(); // CONFIRMED → SHIPPED
order.deliver(); // SHIPPED → DELIVERED
order.cancel(); // throws - cannot cancel a delivered order
Object behaviour changes with its internal state. Illegal transitions throw IllegalStateException rather than silently succeeding. The history list records every transition.
Visitor
// Same shapes, multiple operations - no modification to Shape classes needed
double area = visit(new Circle(5), new AreaVisitor());
double perim = visit(new Circle(5), new PerimeterVisitor());
String desc = visit(new Circle(5), new DescribeVisitor());
Adds new operations to an object hierarchy without modifying it. Modern Java: sealed classes + switch expressions replace the traditional accept(visitor) double-dispatch, as shown here.
Mediator
ChatRoom room = new ChatRoom();
ChatUser alice = new ChatUser("Alice", room);
ChatUser bob = new ChatUser("Bob", room);
alice.join();
bob.join();
alice.send("Hi!"); // Bob receives; Alice does not
bob.leave();
Centralises communication between objects through a single mediator. Reduces N² direct connections to N connections (each user ↔ room only).
Key Takeaways
| Category | Pattern | One-line summary |
|---|---|---|
| Creational | Singleton | One instance, thread-safe via init-on-demand holder |
| Creational | Factory Method | Subclass decides which object to create |
| Creational | Abstract Factory | Create families of related objects |
| Creational | Builder | Fluent construction of complex immutable objects |
| Creational | Prototype | Clone an existing object as a starting point |
| Creational | Object Pool | Reuse expensive-to-create objects |
| Structural | Adapter | Convert incompatible interfaces |
| Structural | Decorator | Add behaviour by wrapping, not subclassing |
| Structural | Facade | Simple interface over a complex subsystem |
| Structural | Composite | Uniform treatment of leaf and branch nodes |
| Structural | Proxy | Controlled access, caching, or lazy loading |
| Structural | Bridge | Decouple abstraction from implementation |
| Structural | Flyweight | Share intrinsic state to reduce memory footprint |
| Behavioural | Observer | Event pub/sub without tight coupling |
| Behavioural | Strategy | Swap algorithms at runtime |
| Behavioural | Command | Encapsulate requests; enable undo/redo |
| Behavioural | Chain of Responsibility | Pass request along a handler chain |
| Behavioural | Template Method | Skeleton algorithm; subclasses fill in steps |
| Behavioural | Iterator | Sequential access without exposing internals |
| Behavioural | State | Behaviour changes with internal state |
| Behavioural | Visitor | Add operations to a hierarchy without modifying it |
| Behavioural | Mediator | Centralise complex communications |