Concepts & Notes

Design Patterns Worth Memorizing

The handful of Gang of Four patterns that actually earn their place in day-to-day code — each with its intent, the problem it solves, and a tight before/after in Java.

Updated OOPDesignReference

The Gang of Four catalogued 23 patterns. You don’t need all of them — most either fell out of favour or show up once a career. This is the short list I actually reach for, grouped by intent: each with a one-line purpose, the problem it fixes, and a minimal before/after.

Strategy

Make a family of algorithms interchangeable by putting each behind a common interface.

The fix for a switch that grows a branch per variant — the same Open/Closed smell. Swap behavior by swapping an object, not by editing a method.

// Before: adding a shipping type reopens this method
double cost(Order o) {
    if (o.ship == STANDARD) return 5.0;
    if (o.ship == EXPRESS)  return 15.0;
    // ...every new option edits tested code
}
// After: a strategy per algorithm; Checkout works with any of them
interface ShippingCost { double of(Order o); }

class Standard implements ShippingCost { public double of(Order o) { return 5.0; } }
class Express  implements ShippingCost { public double of(Order o) { return 15.0; } }

class Checkout {
    private final ShippingCost cost;                 // holds the interface, not a concrete type
    Checkout(ShippingCost cost) { this.cost = cost; }

    double total(Order o) {
        return o.subtotal() + cost.of(o);            // uses whichever strategy it was given
    }
}

// The choice lives outside Checkout — swap the object, don't edit the class
Checkout express = new Checkout(new Express());
Checkout cheap   = new Checkout(new Standard());

Observer

Let objects subscribe to an event source and get notified on change.

Decouples “something happened” from “who cares.” The subject holds no knowledge of its listeners beyond an interface — you add reactions without touching it.

interface OrderListener { void onPlaced(Order o); }

class OrderService {
    private final List<OrderListener> listeners = new ArrayList<>();
    void subscribe(OrderListener l) { listeners.add(l); }

    void place(Order o) {
        // ...persist...
        listeners.forEach(l -> l.onPlaced(o)); // email, analytics, inventory — all opt-in
    }
}

Factory Method

Defer which concrete type to instantiate to a dedicated method or class.

Removes new ConcreteThing() from code that shouldn’t know the concrete type — Dependency Inversion at the point of creation. Callers ask for what they want, not how it’s built.

// Before: caller is welded to the concrete class
Notifier n = new EmailNotifier(smtpConfig);
// After: creation logic lives in one place, caller stays abstract
interface Notifier { void send(String msg); }

class NotifierFactory {
    static Notifier forChannel(Channel c) {
        return switch (c) {
            case EMAIL -> new EmailNotifier(/* ... */);
            case SMS   -> new SmsNotifier(/* ... */);
        };
    }
}

The switch is fine here — it’s isolated to the factory, the one place whose job is knowing the concrete types.

Builder

Construct a complex object step by step, instead of a telescoping constructor.

Kills the new Pizza(true, false, true, false, 12, null) problem: unreadable positional args, and a new constructor for every optional combination.

Pizza p = new Pizza.Builder()
    .size(12)
    .cheese(true)
    .topping("mushroom")
    .build(); // readable, and each field is optional

Adapter

Wrap an incompatible interface so it looks like the one your code expects.

The glue for a third-party or legacy class you can’t change. Your code depends on your interface; the adapter translates.

// You want everything to be a PaymentGateway...
interface PaymentGateway { void charge(long cents); }

// ...but the vendor SDK speaks a different shape
class VendorAdapter implements PaymentGateway {
    private final VendorSdk sdk;
    public void charge(long cents) {
        sdk.makePayment(cents / 100.0, "USD"); // translate the call
    }
}

Decorator

Add behavior to an object by wrapping it, without subclassing.

Layer responsibilities at run time — the alternative to a class explosion like BufferedEncryptedLoggingStream. Each decorator adds one thing and delegates the rest.

interface DataSource { void write(String data); }

class EncryptedSource implements DataSource {
    private final DataSource wrapped; // wraps any DataSource
    public void write(String data) { wrapped.write(encrypt(data)); }
}

// Compose freely: encryption over compression over a file
DataSource ds = new EncryptedSource(new CompressedSource(new FileSource()));

Singleton (know the trade-off)

Guarantee one instance with a global access point.

Included because it’s the most overused pattern — worth knowing precisely so you know when not to reach for it.

enum Config { INSTANCE; /* enum is the thread-safe, serialization-safe way */ }

The catch: a hand-rolled Singleton is hidden global state. It makes code hard to test (you can’t swap it for a fake) and couples everything that touches it. The industry-standard fix is to let a DI container own the single instance — Spring beans are singleton-scoped by default, so you get “exactly one” without the global access point:

@Service // one instance per container, managed for you
class RateLimiter { /* ... */ }

@Service
class OrderService {
    private final RateLimiter limiter;
    OrderService(RateLimiter limiter) { this.limiter = limiter; } // injected, not global
}

Same “one instance” guarantee, but the dependency is explicit in the constructor and trivially swapped for a fake in tests — see Why Constructor Injection Won. Reach for the hand-rolled enum Singleton only outside a DI framework.

At a glance

Pattern Category Use when…
Strategy Behavioral Behavior varies and you’d otherwise switch on a type
Observer Behavioral Many objects must react to an event without the source knowing them
Factory Method Creational Callers shouldn’t know the concrete class they’re getting
Builder Creational A constructor has too many args or optionals
Adapter Structural An external interface doesn’t match what your code expects
Decorator Structural You need to stack optional behaviors without subclass explosion
Singleton Creational You need exactly one — but prefer a DI-managed instance

Patterns are a shared vocabulary, not a checklist to satisfy. Reach for one when you recognize its problem in your code — never bend the code to fit a pattern. The best use of this list is spotting the smell (“this switch keeps growing”) and remembering there’s a named, well-understood shape that fixes it.