Back to Design Pattern Course

Design Pattern Course

Before diving into design patterns, let's look at the SOLID principles.

What is the Purpose of SOLID?

  • To make code more maintainable.
  • To make it easier to extend a system with new functionality without breaking what already works.
  • To make code easier to read and understand, so engineers spend less time figuring out what it does and more time building.
  • The five principles were identified by Robert C. Martin ("Uncle Bob"); the SOLID acronym itself was coined later by Michael Feathers. SOLID is language-agnostic — it applies just as much to C#, TypeScript, or Go as it does to Java.

5 Class Design Principles (SOLID)

S : Single Responsibility Principle

O : Open/Closed Principle

L : Liskov Substitution Principle

I : Interface Segregation Principle

D : Dependency Inversion Principle

These are guidelines, not laws. Over-applying them to trivial code — an interface for every class "because SOLID" when there's only ever one implementation — creates needless indirection. Good engineers apply SOLID where it earns its complexity, not everywhere uniformly.

Single Responsibility Principle (SRP)

A class should have only one reason to change — i.e., it should have a single responsibility.

public class UserService { public void validate(User user) { // validation logic } public void makePayment(User fromUser, User toUser) { // payment logic } }

What changes can be made to better follow SRP?

public class UserValidationService { public void validate(User user) { // validation logic } } public class PaymentService { public void makePayment(User fromUser, User toUser) { // payment logic } }

Each class now has a single responsibility — when payment logic changes, only PaymentService is touched, and validation tests aren't put at risk by an unrelated payment refactor. "Reason to change" should be read as "driven by a different stakeholder or concern," not "any possible edit" — don't fragment classes into single-method pieces just to satisfy a literal reading of the rule. In a microservices context, this same idea applies at the service level: a service that both validates users and processes payments is usually a sign it should be split into two independently deployable services.

Open/Closed Principle (OCP)

Software entities (classes, modules, functions) should be open for extension but closed for modification — you should be able to add new behavior without editing existing, already-tested code.

The Factory and Strategy patterns are classic ways to achieve this (see those articles later in this series).

When OCP is followed correctly, existing code remains unchanged and all existing tests continue to pass when new functionality is added.

Liskov Substitution Principle (LSP)

Named after Barbara Liskov. Subtypes must be substitutable for their base types without altering the correctness of the program — code written against a base class or interface should keep working correctly if any subclass is swapped in, with no weakened guarantees or unexpected exceptions. The textbook violation is Square extends Rectangle (setting the width on a Square unexpectedly changes its height too). In real codebases, LSP violations show up more often as a subclass throwing UnsupportedOperationException for a method its parent promises to support — a classic smell in collection or repository class hierarchies.

Interface Segregation Principle (ISP)

Clients should not be forced to depend on methods they do not use. It's better to have several small, role-specific interfaces than one large "fat" interface.

Dependency Inversion Principle (DIP)

High-level modules should not depend on low-level modules; both should depend on abstractions. Abstractions should not depend on details — details should depend on abstractions.

In Spring, this is exactly what wiring against an interface (rather than a concrete class) and the IoC container give you for free — your service depends on a Repository interface, not a specific JDBC or JPA implementation, and the container injects the concrete implementation at runtime.

Why Design Patterns?

  1. Design patterns are reusable, proven solutions to commonly recurring problems in software design.
  2. A design pattern describes how to solve a problem in a given context — it's not a finished block of code you copy-paste verbatim.
  3. Design patterns are not a technology, tool, language, or framework — they're a shared vocabulary for communicating design intent between engineers.
  4. Because they're proven, they reduce risk: the trade-offs are well understood instead of being discovered the hard way in production.

Where Did Design Patterns Come From?

The catalog of 23 classic design patterns comes from the 1994 book Design Patterns: Elements of Reusable Object-Oriented Software, written by Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides — nicknamed the "Gang of Four" (GoF). This was an independent authorship effort by four engineers, not a group convened by Sun Microsystems (a common but inaccurate claim).

The GoF organized their 23 patterns into three categories: Creational, Structural, and Behavioral.

Worth knowing: the GoF book predates Java, dependency-injection frameworks, lambdas, and functional interfaces — its examples were originally in C++ and Smalltalk. Several patterns that needed a full class hierarchy in 1994 (Strategy, Command, Observer) are now often implemented with a one-line lambda or a java.util.function interface in modern Java. The classic structure is still worth knowing — it's the vocabulary the industry uses in design discussions and interviews — but in real 2026-era code, don't force a multi-class hierarchy where a method reference does the job.

screenshot of the app

Creational Patterns

Creational patterns abstract away the instantiation process — they make a system independent of how its objects are created, composed, and represented.

  • Singleton: Ensure a class has only one instance and provide a global point of access to it.
  • Builder: Separate the construction of a complex object from its representation, so the same construction process can create different representations.
  • Factory Method: Define an interface for creating an object, but let subclasses decide which class to instantiate.
  • Abstract Factory: Provide an interface for creating families of related or dependent objects without specifying their concrete classes.
  • Prototype: Specify the kinds of objects to create using a prototypical instance, and create new objects by copying (cloning) that prototype.

Structural Patterns

Structural patterns deal with how classes and objects are composed to form larger structures, typically combining inheritance and composition.

  • Bridge: Decouple an abstraction from its implementation so the two can vary independently.
  • Adapter: Convert the interface of a class into another interface clients expect, letting otherwise-incompatible classes work together.
  • Composite: Compose objects into tree structures to represent part-whole hierarchies, letting clients treat individual objects and compositions of objects uniformly. (Often mistyped as "compose objects into three objects" — it's a tree structure, not a count of three.)
  • Decorator: Attach additional responsibilities to an object dynamically — a flexible alternative to subclassing for extending behavior.
  • Proxy: Provide a surrogate or placeholder for another object to control access to it.
  • Facade: Provide a unified, higher-level interface to a set of interfaces in a subsystem, making the subsystem easier to use.
  • Flyweight: Use sharing to support large numbers of fine-grained objects efficiently.

Behavioral Patterns

Behavioral patterns are concerned with algorithms and the assignment of responsibilities between objects — how objects interact and distribute work while staying loosely coupled.

  • Template Method: Define the skeleton of an algorithm in a method, deferring some steps to subclasses, without letting subclasses change the algorithm's overall structure.
  • Mediator: Promote loose coupling by keeping objects from referring to one another explicitly, letting their interaction vary independently.
  • Strategy: Define a family of algorithms, encapsulate each one, and make them interchangeable, so the algorithm can vary independently of the clients that use it.
  • State: Allow an object to alter its behavior when its internal state changes, so it appears to change its class.
  • Chain of Responsibility: Avoid coupling the sender of a request to its receiver by giving more than one object a chance to handle the request; pass the request along a chain until something handles it.
  • Observer: Define a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically.
  • Command: Encapsulate a request as an object, letting you parameterize clients with different requests, queue or log requests, and support undoable operations.
  • Visitor: Represent an operation to be performed on the elements of an object structure, letting you define a new operation without changing the classes of the elements it operates on.
  • Iterator: Provide a way to access the elements of an aggregate object sequentially without exposing its underlying representation.
  • Interpreter: Given a language, define a representation for its grammar along with an interpreter that uses the representation to interpret sentences in that language.
  • Memento: Without violating encapsulation, capture and externalize an object's internal state so it can be restored to that state later.

screenshot of the app