Design Principles & Patterns — Cheat Sheet

SOLID · DRY · KISS · YAGNI · GoF patterns

§ Principles

SOLID — five object-oriented design principles
SSingle Responsibility
A class should have only one reason to change
// ✗ BAD — one class doing too much
class Invoice {
  void calculateTotal() { ... }
  void printInvoice()    { ... }  // print concern
  void saveToDB()       { ... }  // persistence concern
}
// ✓ GOOD — each class owns one job
class Invoice        { void calculateTotal() {} }
class InvoicePrinter { void print(Invoice i)  {} }
class InvoiceRepo    { void save(Invoice i)  {} }
OOpen / Closed
Open for extension, closed for modification
// Open for extension, closed for modification
interface Shape   { double area(); }
class Circle      implements Shape { public double area() { return Math.PI * r * r; } }
class Rectangle   implements Shape { public double area() { return w * h; } }
// AreaCalculator never changes — just add new Shape impls
class AreaCalculator {
  double total(List<Shape> shapes) {
    return shapes.stream().mapToDouble(Shape::area).sum();
  }
}
LLiskov Substitution
Subtypes must be substitutable for their base type
// Subtype must be substitutable for its base type
class Bird { void fly() {} }
// ✗ Penguin can't fly — violates LSP
class Penguin extends Bird {
  @Override void fly() { throw new UnsupportedOperationException(); }
}
// ✓ Fix: separate FlyingBird interface
interface FlyingBird { void fly(); }
class Sparrow implements FlyingBird { public void fly() {} }
class Penguin2 { /* no fly() at all */ }
IInterface Segregation
No client should depend on methods it doesn't use
// ✗ Fat interface forces no-op impls
interface Worker { void work(); void eat(); }
class Robot implements Worker {
  public void work() {}
  public void eat()  { throw new UnsupportedOperationException(); }
}
// ✓ Segregated — clients get only what they need
interface Workable { void work(); }
interface Eatable  { void eat();  }
class Human implements Workable, Eatable { ... }
class Robot2 implements Workable         { ... }
DDependency Inversion
Depend on abstractions, not concretions
// ✗ High-level depends on concrete low-level
class OrderService { private MySQLRepo repo = new MySQLRepo(); }

// ✓ Both depend on the abstraction
interface OrderRepo  { void save(Order o); }
class MySQLRepo     implements OrderRepo { ... }
class OrderService  {
  private final OrderRepo repo;              // injected
  OrderService(OrderRepo repo) { this.repo = repo; }
}
DRY — Don't Repeat Yourself
Every piece of knowledge should have a single, unambiguous representation in the system

Violations look like

copy-paste logicmagic numbersduplicate SQLrepeated validation

Fixes

extract methodshared constantssingle DB schemashared validators

Caution

Don't apply DRY to coincidental duplication — two things that look alike now may diverge later. Abstract only when the duplication represents the same concept.

KISS — Keep It Simple, Stupid
Prefer the simplest solution that correctly solves the problem — add complexity only when proven necessary

Symptoms of violation

deep inheritancepremature abstractionover-engineeringunnecessary patterns

Related heuristics

Rule of ThreeOccam's RazorWorse is Better

Note

Simple ≠ simplistic. The goal is to eliminate accidental complexity while preserving the essential complexity the problem demands.

YAGNI — You Aren't Gonna Need It
Don't implement something until it is actually needed — not when you merely predict you'll need it

Classic traps

unused config flagsspeculative generalityhooks for "future" featurespre-built plugin systems

Origin

Coined in Extreme Programming (XP) by Ron Jeffries. Works hand-in-hand with KISS — if you don't need it now, adding it only adds complexity now.

The cost of premature features

Code written speculatively still needs to be read, tested, maintained, and worked around — even when it's never used.

§ Design Patterns

Singleton — Creational · ensures only one instance exists

Problem solved

Multiple instances of a shared resource (DB connection pool, config, logger) wasting memory or causing inconsistency.

Why needed

Some objects should exist exactly once. Constructors don't enforce uniqueness.

In Java / JDK

Runtime.getRuntime()Collections.EMPTY_LISTSpring beans (default)java.lang.System
1.Eager initThread-safe
// 1. Eager — instance created at class load
class Singleton {
  private static final Singleton INSTANCE = new Singleton();
  private Singleton() {}
  public static Singleton getInstance() { return INSTANCE; }
}
2.Static blockThread-safe
// 2. Static block — allows exception handling at init
class Singleton {
  private static final Singleton INSTANCE;
  static { try { INSTANCE = new Singleton(); } catch (Exception e) { throw new RuntimeException(e); } }
  private Singleton() {}
  public static Singleton getInstance() { return INSTANCE; }
}
3.Lazy initNot thread-safe
// 3. Lazy — created on first use (not thread-safe)
class Singleton {
  private static Singleton instance;
  private Singleton() {}
  public static Singleton getInstance() {
    if (instance == null) instance = new Singleton();
    return instance;
  }
}
4.Synchronized methodSlow
// 4. Thread-safe (synchronized) — slow, acquires lock every call
public static synchronized Singleton getInstance() {
  if (instance == null) instance = new Singleton();
  return instance;
}
5.Double-checked lockingFast + safe
// 5. Double-checked locking — volatile stops CPU reordering
private volatile static Singleton instance;
public static Singleton getInstance() {
  if (instance == null) {
    synchronized (Singleton.class) {
      if (instance == null) instance = new Singleton();
    }
  }
  return instance;
}
6.Bill Pugh (Holder)★ Recommended
// 6. Bill Pugh (static inner class) — ✦ RECOMMENDED
//    JVM loads inner class only when getInstance() is called
class Singleton {
  private Singleton() {}
  private static class Holder {
    static final Singleton INSTANCE = new Singleton();
  }
  public static Singleton getInstance() { return Holder.INSTANCE; }
}
7.Enum★ Reflection-safe
// 7. Enum — ✦ serialisation + reflection safe (Joshua Bloch)
public enum Singleton {
  INSTANCE;
  public void doSomething() { ... }
}
// usage:
Singleton.INSTANCE.doSomething();
8.Serializable★ Serialize-safe
// 8. Serializable Singleton — survives serialize/deserialize
//    readResolve() intercepts deserialization; returns INSTANCE
//    instead of creating a brand-new object from the stream
class Singleton implements Serializable {
  private static final long serialVersionUID = 1L;
  private static final Singleton INSTANCE = new Singleton();
  private Singleton() {}
  public static Singleton getInstance() { return INSTANCE; }

  // ★ The magic hook — called by ObjectInputStream after
  //   constructing the object from the byte stream
  protected Object readResolve() { return INSTANCE; }
}
// verify identity survives round-trip:
// deserialised == Singleton.getInstance()  →  true
Why #8 matters · Without readResolve(), Java's default deserialization calls ObjectInputStream.readObject() which allocates a new object from the byte stream — bypassing the private constructor and breaking the singleton guarantee. The hook returns the existing INSTANCE, so the JVM discards the freshly allocated object and the reference stays unique. Prefer Enum (#7) when serialization safety is the only concern — it handles this automatically.
Strategy — Behavioural · swap algorithms at runtime

Problem solved

Bloated if/else chains that change algorithm behaviour based on type or context.

Why needed

Open/Closed — add a new strategy without touching the context class.

In Java / JDK

Comparator (Collections.sort)ExecutorService policiesSpring Security AuthenticationProvider
interface SortStrategy { void sort(int[] arr); }
class BubbleSort    implements SortStrategy { public void sort(int[] a) { ... } }
class QuickSort     implements SortStrategy { public void sort(int[] a) { ... } }

class Sorter {
  private SortStrategy strategy;
  Sorter(SortStrategy s) { this.strategy = s; }
  void sort(int[] arr)        { strategy.sort(arr); }
}

// runtime swap — no if/else chain
Sorter s = new Sorter(new QuickSort());
s.sort(data);
Builder — Creational · construct complex objects step-by-step

Problem solved

Telescoping constructors — hard to read when an object has many optional parameters.

Why needed

Separates construction from representation; enforces valid state at build().

In Java / JDK

StringBuilderHttpClient.newBuilder()Lombok @BuilderStream.Builder
class HttpRequest {
  private final String url, method, body;
  private final int timeout;
  private HttpRequest(Builder b) {
    url = b.url; method = b.method; body = b.body; timeout = b.timeout;
  }
  static class Builder {
    String url, method = "GET", body; int timeout = 5000;
    Builder url(String v)     { url = v; return this; }
    Builder method(String v)  { method = v; return this; }
    Builder body(String v)    { body = v; return this; }
    Builder timeout(int v)    { timeout = v; return this; }
    HttpRequest build()       { return new HttpRequest(this); }
  }
}
// usage — reads like English, order doesn't matter
HttpRequest req = new HttpRequest.Builder()
  .url("https://api.example.com")
  .method("POST").body("{}").timeout(3000)
  .build();
Decorator — Structural · add behaviour without subclassing

Problem solved

Subclass explosion — every combination of features needs a new class.

Why needed

Wraps existing objects at runtime; honours the same interface. Composable.

In Java / JDK

java.io (BufferedReader wraps FileReader)Collections.synchronizedList()HttpServletRequestWrapper
interface Coffee   { String desc(); double cost(); }
class Espresso     implements Coffee  {
  public String desc() { return "Espresso"; }
  public double cost() { return 1.0; }
}
abstract class CoffeeDecorator implements Coffee {
  protected final Coffee wrapped;
  CoffeeDecorator(Coffee c) { wrapped = c; }
}
class Milk extends CoffeeDecorator {
  Milk(Coffee c) { super(c); }
  public String desc() { return wrapped.desc() + ", Milk"; }
  public double cost() { return wrapped.cost() + 0.25; }
}
// stack decorators at runtime — no subclass explosion
Coffee c = new Milk(new Espresso());  // "Espresso, Milk"
Factory Method — Creational · delegate object creation to a method

Problem solved

Clients hard-coded to concrete classes — switching implementations breaks code.

Why needed

Centralises creation logic; client depends only on the interface, not the impl.

In Java / JDK

Calendar.getInstance()NumberFormat.getInstance()LoggerFactory.getLogger()valueOf() methods
interface Notification   { void send(String msg); }
class EmailNotification   implements Notification { public void send(String m) { ... } }
class SMSNotification     implements Notification { public void send(String m) { ... } }
class PushNotification    implements Notification { public void send(String m) { ... } }

class NotificationFactory {
  public static Notification create(String type) {
    return switch (type) {
      case "EMAIL" -> new EmailNotification();
      case "SMS"   -> new SMSNotification();
      case "PUSH"  -> new PushNotification();
      default       -> throw new IllegalArgumentException(type);
    };
  }
}
// client never sees concrete classes
NotificationFactory.create("EMAIL").send("Hello");
Observer — Behavioural · one-to-many event notification

Problem solved

Tight coupling between a state-changing object and all objects that need to react to that change.

Why needed

Decouples publisher from subscribers — add/remove listeners without modifying the subject.

In Java / JDK

java.util.EventListenerSpring ApplicationEventRxJava / Reactorjavax.swing listeners
// Subject notifies all registered observers on state change
interface EventListener { void onEvent(String data); }

class EventBus {
  private final List<EventListener> listeners = new ArrayList<>();
  void subscribe(EventListener l) { listeners.add(l); }
  void publish(String data) {
    listeners.forEach(l -> l.onEvent(data));
  }
}
// observers are decoupled from the publisher
EventBus bus = new EventBus();
bus.subscribe(msg -> log(msg));
bus.subscribe(msg -> sendEmail(msg));
bus.publish("order.placed");
Adapter — Structural · bridge incompatible interfaces

Problem solved

Existing class has the right logic but the wrong interface — can't use it without modification.

Why needed

Wrap the incompatible class so it conforms to the expected interface without changing source.

In Java / JDK

Arrays.asList()InputStreamReaderCollections.enumeration()Spring HandlerAdapter
// Convert incompatible interface to expected one
interface MediaPlayer    { void play(String file); }
interface AdvancedPlayer { void playMp4(String f); }

class Mp4Player implements AdvancedPlayer {
  public void playMp4(String f) { ... }
}

class MediaAdapter implements MediaPlayer {
  private final AdvancedPlayer advanced;
  MediaAdapter(AdvancedPlayer a) { advanced = a; }
  public void play(String f) { advanced.playMp4(f); }
}
// client uses MediaPlayer interface — doesn't know about Mp4Player
MediaPlayer p = new MediaAdapter(new Mp4Player());
p.play("video.mp4");
Abstract Factory — Creational · families of related objects

Problem solved

Need to create families of related objects (e.g. UI widgets per OS) that must be consistent with each other.

Why needed

Guarantees product compatibility; swap an entire product family by swapping the factory.

In Java / JDK

javax.xml.parsers.DocumentBuilderFactoryjava.sql (Driver → Connection → Statement)Spring DataAccessStrategy
// Family of related objects — e.g. cross-platform UI
interface Button  { void render(); }
interface Checkbox{ void render(); }

interface UIFactory {
  Button   createButton();
  Checkbox createCheckbox();
}
class WindowsFactory implements UIFactory {
  public Button   createButton()   { return new WinButton(); }
  public Checkbox createCheckbox() { return new WinCheckbox(); }
}
class MacFactory     implements UIFactory {
  public Button   createButton()   { return new MacButton(); }
  public Checkbox createCheckbox() { return new MacCheckbox(); }
}
// client stays decoupled from OS — swap factory = swap platform
UIFactory f = new WindowsFactory();
f.createButton().render();