§ Principles
// ✗ 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) {} }
// 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(); } }
// 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 */ }
// ✗ 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 { ... }
// ✗ 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; } }
Violations look like
Fixes
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.
Symptoms of violation
Related heuristics
Note
Simple ≠ simplistic. The goal is to eliminate accidental complexity while preserving the essential complexity the problem demands.
Classic traps
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
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
// 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 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 — 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. Thread-safe (synchronized) — slow, acquires lock every call public static synchronized Singleton getInstance() { if (instance == null) instance = new Singleton(); return instance; }
// 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 (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 — ✦ serialisation + reflection safe (Joshua Bloch) public enum Singleton { INSTANCE; public void doSomething() { ... } } // usage: Singleton.INSTANCE.doSomething();
// 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
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.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
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);
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
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();
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
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"
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
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");
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
// 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");
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
// 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");
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
// 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();