Design Patterns in Java

Introduction to Design Patterns

15 min Lesson 1 of 13

Introduction to Design Patterns

You have written object-oriented code, applied SOLID principles, used generics and streams, and wrangled concurrency. You know how to write Java. Design patterns answer a different question: given a recurring structural problem, what is the proven, idiomatic way to solve it? This lesson answers that question and maps the landscape of the 23 canonical patterns introduced by the Gang of Four.

What Is a Design Pattern?

A design pattern is a named, reusable solution to a commonly occurring problem in a given context. The term was popularised in 1994 by the book Design Patterns: Elements of Reusable Object-Oriented Software by Gamma, Helm, Johnson, and Vlissides — universally known as the Gang of Four (GoF). The book catalogued 23 patterns with:

  • A name — a shared vocabulary so teams can communicate intent instantly.
  • The problem — the context and forces that demand the pattern.
  • The solution — the abstract structural description (participants, collaborations).
  • The consequences — trade-offs in flexibility, complexity, and performance.
A pattern is not code — it is a blueprint. You do not paste a pattern; you apply it. The same conceptual pattern may look slightly different in every codebase depending on language features, frameworks, and constraints. Java 17 records, sealed classes, and lambdas let you implement patterns far more concisely than the original C++ examples in the GoF book.

Why Design Patterns Matter

Experienced engineers reach for patterns because they solve real professional pain points.

Shared Vocabulary

Saying "use a Strategy here" in a code review instantly communicates structure, intent, and expected trade-offs. Without patterns, the same discussion takes paragraphs of prose.

Proven Trade-Off Analysis

Every pattern comes with documented consequences. The Singleton saves memory but introduces global mutable state and complicates testing. Knowing the trade-offs up front prevents architectural regret later.

Guidance When Requirements Change

Patterns anticipate the axes along which software evolves. The Open/Closed Principle is aspirational; patterns are the mechanics that make it achievable. The Decorator pattern, for example, lets you add responsibilities to objects without touching existing classes — critical in codebases you cannot safely modify.

A Framework for Recognising Smells

Knowing patterns makes their absence visible. A switch on a type field scattered across dozens of methods is a signal that the Strategy or Visitor pattern is missing. You can name what is wrong and articulate the fix.

The Three GoF Categories

The 23 GoF patterns are divided into three categories based on what aspect of object structure they address.

1. Creational Patterns

Creational patterns are concerned with how objects are created. They decouple the creation mechanism from the code that uses the object, giving you flexibility over what is created, who creates it, and when.

  • Singleton — ensures a class has exactly one instance and provides a global access point.
  • Factory Method — defines an interface for creating an object but lets subclasses decide which class to instantiate.
  • Abstract Factory — provides an interface for creating families of related objects without specifying concrete classes.
  • Builder — separates the construction of a complex object from its representation, allowing the same construction process to produce different results.
  • Prototype — creates new objects by copying an existing object (cloning).
// Without a pattern: callers are coupled to concrete types Connection conn = new MySQLConnection("host", "user", "pass"); // With Factory Method: callers depend only on the abstraction Connection conn = ConnectionFactory.create(config); // The factory decides whether to return a MySQLConnection, H2Connection, etc.

2. Structural Patterns

Structural patterns are concerned with how classes and objects are composed to form larger structures. They make it easier to combine independent pieces without tight coupling.

  • Adapter — converts the interface of a class into another interface clients expect.
  • Bridge — separates an abstraction from its implementation so both can vary independently.
  • Composite — composes objects into tree structures to represent part-whole hierarchies.
  • Decorator — attaches additional responsibilities to an object dynamically.
  • Facade — provides a simplified interface to a complex subsystem.
  • Flyweight — uses sharing to efficiently support a large number of fine-grained objects.
  • Proxy — provides a surrogate or placeholder for another object to control access.
// Decorator example — wrap a Writer to add buffering and encryption, // without changing the Writer interface or its existing implementations Writer base = new FileWriter("output.txt"); Writer buf = new BufferedWriter(base); Writer secure = new EncryptingWriter(buf); // custom Decorator secure.write("sensitive data");

3. Behavioral Patterns

Behavioral patterns are concerned with algorithms and the assignment of responsibility between objects. They describe how objects communicate and distribute work.

  • Chain of Responsibility — passes a request along a chain of handlers.
  • Command — encapsulates a request as an object, allowing parameterization and undo.
  • Interpreter — defines a grammar and an interpreter for a language.
  • Iterator — provides a way to sequentially access elements of a collection without exposing its internal representation.
  • Mediator — defines an object that encapsulates how a set of objects interact.
  • Memento — captures and externalises an object's internal state so it can be restored later.
  • Observer — defines a one-to-many dependency so that when one object changes state all dependents are notified.
  • State — allows an object to alter its behaviour when its internal state changes.
  • Strategy — defines a family of algorithms, encapsulates each one, and makes them interchangeable.
  • Template Method — defines the skeleton of an algorithm in a base class, deferring some steps to subclasses.
  • Visitor — lets you define a new operation on elements of a structure without changing the classes of those elements.
// Strategy — swap the sorting algorithm at runtime without touching the caller @FunctionalInterface interface SortStrategy<T extends Comparable<T>> { void sort(List<T> list); } class ReportService { private final SortStrategy<String> strategy; ReportService(SortStrategy<String> strategy) { this.strategy = strategy; } List<String> generate(List<String> rows) { var copy = new ArrayList<>(rows); strategy.sort(copy); // delegate — no switch, no instanceof return copy; } } // At the call site, pass a lambda or a method reference var service = new ReportService(Collections::sort); var reversed = new ReportService((list) -> list.sort(Comparator.reverseOrder()));

Patterns Are Context-Dependent

No pattern is universally correct. Applying a pattern where it is not warranted adds accidental complexity — an anti-pattern in its own right. Before reaching for a pattern, verify two things:

  1. The problem you are facing genuinely recurs and matches the pattern's stated problem context.
  2. The trade-offs (typically more classes, more indirection) are worth the flexibility gained.
Start simple, refactor to a pattern. The idiomatic Java approach is to write the simplest code that works, then introduce a pattern when a specific force — a new requirement, a test-seam need, a violation of Open/Closed — makes it necessary. Patterns discovered by refactoring are almost always better fits than patterns applied up front.
Pattern overuse (pattern fever) is a real hazard. A codebase full of unnecessary Factories, Decorators, and Strategies is harder to read than one that uses direct instantiation and polymorphism. The GoF themselves warned that applying patterns without understanding their consequences leads to over-engineered, brittle designs. Every pattern should be justified by a concrete design pressure, not imposed for its own sake.

Patterns in the Java Ecosystem

Design patterns are not academic exercises — they are deeply embedded in the Java platform and its most important frameworks:

  • Iterator — every java.util.Collection exposes one via Iterable.
  • Decoratorjava.io.BufferedInputStream, java.io.GZIPOutputStream.
  • Factory MethodList.of(), Optional.of(), Path.of().
  • SingletonRuntime.getRuntime(), Spring's default bean scope.
  • Proxy — Spring AOP, JDK dynamic proxies for interfaces.
  • Observerjava.util.EventListener, reactive frameworks (Project Reactor, RxJava).
  • Template Method — Spring's JdbcTemplate, RestTemplate.
  • BuilderStringBuilder, Stream.Builder, Lombok's @Builder.

Recognising these patterns in production libraries transforms you from a user of the API into someone who understands the architecture behind it — a critical distinction at the senior and staff engineer level.

Summary

A design pattern is a named, proven solution to a recurring design problem. The GoF catalogued 23 patterns in three categories: creational (object creation), structural (composition), and behavioral (communication and responsibility). Patterns give teams a shared vocabulary, encode hard-won trade-off knowledge, and guide designs toward extensibility. They are most valuable when applied in response to a real design pressure — not preemptively. In the following lessons you will implement each category's most important patterns in idiomatic Java 17, learning not just the structure but the forces that justify each choice.