Patterns you already use

Decorator, chain of responsibility, observer and template method — named in the JDK and Spring code you already rely on.

6 min read📐 Low-Level Design

You have been using design patterns for as long as you have been writing Java, without the names. That is the right order — a pattern learned as a name is trivia; a pattern recognised in code you already rely on is a tool.

This lesson names four you have definitely met.

Decorator — every stream you have ever opened

java
new BufferedReader(new InputStreamReader(System.in))

Three objects, each wrapping the next, each adding one thing without changing the interface. InputStreamReader turns bytes into characters. BufferedReader adds buffering. Both are still a Reader, so a method taking a Reader neither knows nor cares how deep the stack goes.

That is decorator: add behaviour by wrapping, not by subclassing. And the reason it beats subclassing is combinatorial — buffered, gzipped, encrypted and counted is four decorators and one wrapping expression, where subclassing would need a class per combination.

Its value is measurable. The reading-input lesson summed 300,000 integers two ways:

plaintext
### Scanner:          170 ms
### BufferedReader:    14 ms

Twelve times, from one decorator. Nothing else in the program changed.

Where else you have met it: Collections.unmodifiableList wraps a list and removes mutation; Spring wraps a DataSource to add transaction awareness; every HttpServletRequestWrapper in a filter.

Chain of responsibility — the filter chain you configured

The Spring Security lesson printed this, from an application with no configuration at all:

plaintext
### chain 1 has 16 filters, in this order:
###    3. SecurityContextPersistenceFilter
###    5. CsrfFilter
###   13. AnonymousAuthenticationFilter
###   15. ExceptionTranslationFilter
###   16. FilterSecurityInterceptor

Sixteen handlers, each one deciding whether to act, whether to stop the request, and whether to pass it on. That is chain of responsibility, and the whole of servlet filters, Spring Security and most middleware is this one pattern.

What it buys is that the handlers do not know about each other. CsrfFilter has never heard of BasicAuthenticationFilter. You add one by inserting it into the chain, and nothing else changes — which is why "add a filter, in what order" is a configuration question rather than a code change.

The cost is the other side of the same property: the behaviour is in the order, and the order is not in any one file. A request that mysteriously 403s is a chain question, and the only way to answer it is to print the chain — which is why that lesson starts by printing it.

Observer — events, and everything built on them

One object changes; others need to know; and the one that changed should not have to know who they are.

java
@Component
class OrderService {
    private final ApplicationEventPublisher events;
    void place(Order order) {
        repository.save(order);
        events.publishEvent(new OrderPlaced(order.getId()));   // does not know who listens
    }
}
 
@Component
class EmailListener {
    @EventListener
    void on(OrderPlaced e) { ... }
}

OrderService has no reference to EmailListener and no import of it. You add an analytics listener by writing a class, and the order service does not change — which is the point.

The same shape, scaled up, is the whole of event-driven architecture: publish an event, let subscribers react, and the publisher stays ignorant of them. Kafka is observer with a network and durability in the middle.

The costs are real and worth stating, because this pattern is the easiest to overuse:

  • Control flow becomes invisible. You cannot find out what happens after place() by reading place(). The answer is spread across every class with a listener.
  • Ordering is not guaranteed unless you ask for it.
  • In Spring, an @EventListener runs synchronously in the caller's thread by default — including inside the caller's transaction. So a slow listener slows the order, and a listener that throws rolls the order back. @TransactionalEventListener(phase = AFTER_COMMIT) is usually what people meant.

Strategy — every comparator you have written

Pass the varying behaviour in as an object, instead of branching on a type:

java
list.sort(Comparator.comparing(Order::total));       // the comparator IS the strategy
jdbcTemplate.query(sql, rowMapper);                  // so is the row mapper

Comparator is the pattern in its purest form: sort does not know how you want things ordered, so ordering is a parameter. Every lambda you pass to a method that varies behaviour is a strategy, which is why the pattern feels invisible in modern Java — it stopped needing a class.

What it replaces is a conditional that grows:

java
// the shape strategy removes
if (method.equals("card"))      { /* card logic */ }
else if (method.equals("upi"))  { /* upi logic */ }
else if (method.equals("netbanking")) { ... }
 
// the shape it becomes
interface PaymentProcessor { Receipt charge(Money m); }
Map<String, PaymentProcessor> byMethod;               // Spring will inject this map for you
byMethod.get(method).charge(amount);

The gain is not that the if disappeared — it is that adding a payment method is a new class and no edit to existing code. Spring makes this particularly cheap: inject Map<String, PaymentProcessor> and the container fills it with every bean of that type, keyed by bean name.

The costs, which the last lesson in this course insists on naming:

  • Finding which implementation ran is now a question. It was a line of the if; it is now a map lookup and a bean name.
  • It is only worth it with several implementations. One strategy with one implementation is an interface, a class and a map, replacing three lines you could read.

That second point is the whole test, and it is why this pattern is both the most useful and the most overused on the list.

Template method — the skeleton with holes

The superclass owns the shape of an algorithm; subclasses fill in steps:

java
abstract class Importer {
    final void run() {          // final: the shape is not negotiable
        validate();
        Data d = parse();       // subclass decides
        save(d);
        notifyDone();
    }
    protected abstract Data parse();
}

You have met it in AbstractList, where get and size are yours and everything else is written; in InputStream, where read() is the one abstract method; and in Spring's JdbcTemplate, which owns connection handling and error translation and leaves you the statement — the name is not an accident.

The final on run() matters: it is what makes it a template rather than a suggestion.

Where it has been superseded: in modern Java, a lambda often does the same job with less ceremony. JdbcTemplate.query(sql, rowMapper) passes the varying step as an argument instead of requiring a subclass — which is strategy, and it composes better. Reach for template method when the varying steps are several and related; for one varying step, pass a function.

Why naming them is worth anything

Not for interviews. Three concrete reasons:

  • You recognise the shape in unfamiliar code. Seeing new X(x) and knowing "decorator" tells you what to look for without reading the class.
  • You can say what you mean in review — "this is a chain, and the order is the behaviour" is one sentence instead of five.
  • You inherit the known costs. Every pattern has them, and they are documented. Choosing observer means choosing invisible control flow; knowing that in advance is the difference between a decision and a surprise.
Progress is saved on this device and to your account when signed in.