OOP Design Patterns

Reach for proven blueprints instead of reinventing the wheel. You'll learn the seven Gang of Four patterns you actually meet in Java — and, just as importantly, when to leave them out.

Learn OOP Design Patterns in our free Java course — a beginner-friendly interactive lesson with worked examples, a practice exercise and a quick reference.

Part of the free Java course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.

Design patterns are like standard blueprints a builder reuses . Nobody redesigns a staircase from scratch on every house — there's a known-good shape for it. Patterns are the same: named, battle-tested solutions to problems that keep coming back, so you don't reinvent (or re-break) them.

Each pattern is a tiny story you can picture:

Singleton = the one CEO. Factory = an order desk that hands you the right product. Builder = ordering a custom sandwich, one topping at a time.

Decorator = gift-wrapping — extra layers, same gift. Adapter = a travel plug that makes a UK charger fit a US socket.

Strategy = choosing a route on a sat-nav (fastest vs shortest). Observer = a newsletter — subscribe once, get every update automatically.

Patterns fall into three families : Creational (how objects are made — Singleton, Factory, Builder), Structural (how objects are composed — Decorator, Adapter), and Behavioral (how objects talk to each other — Strategy, Observer).

Creational patterns answer one question: how do you make an object? Sometimes you want exactly one of something, sometimes you want to hide which subclass gets built, and sometimes the object is too fiddly for a plain constructor.

A Singleton guarantees a class has a single shared instance — handy for a configuration object, a connection pool, or a logger. In Java the cleanest version is an enum with a single value; the JVM hands you a thread-safe, one-and-only instance with no extra code:

When creation is expensive and you want to defer it, use lazy initialisation with double-checked locking and a volatile field so concurrent threads don't each build their own copy.

A Factory centralises the new keyword. Instead of scattering new Circle(...) and new Square(...) around your code, callers ask the factory for a Shape and get the right subtype back — so adding a new shape is a one-line change in one place.

Imagine a constructor with eight parameters, half of them optional and three of them boolean . Calling new Burger("brioche", true, false, true, ...) is unreadable, and one wrong argument order is a silent bug. The Builder pattern fixes this: you set each part by name, chaining calls, then ask build() for the finished object.

Each setter returns this , which is what lets the calls chain. The real power move is putting your validation inside build() — if something's wrong, the object never gets created, so an invalid Burger can't exist anywhere in your program.

When to use: objects with many optional fields, configuration objects, SQL query construction, or any time a constructor would need more than three or four arguments.

This factory is almost done. Fill in the three blanks so it returns the right Notifier for each channel. The expected output is written in the comments — run it and check.

Structural patterns are about how objects fit together . Two you'll use constantly are Decorator and Adapter. They look similar — both wrap another object — but they exist for opposite reasons.

Decorator — add behaviour, keep the interface.

A Decorator wraps an object to extend it without touching the original class . A milk-wrapped coffee is still a Coffee , so you can stack wrappers as deep as you like: Whip(Sugar(Milk(coffee))) . This is exactly how Java's BufferedReader(new FileReader(...)) works.

An Adapter makes an incompatible class usable. Say a third-party library gives you an XmlReport with a method called toXmlString() , but your code expects the Report interface with render() . You can't edit the library — so you write a small adapter that implements Report and forwards render() to toXmlString() .

The key distinction: a Decorator keeps the same interface and adds to it; an Adapter translates one interface into another. Decorator enhances; Adapter bridges.

Behavioral patterns describe how objects collaborate and pass responsibility around . The two workhorses are Strategy and Observer.

A Strategy lets you choose how something is done, on the fly. A checkout doesn't care whether you pay by card or PayPal — it just calls pay(amount) on whichever strategy you hand it. Adding a new payment method means writing a new strategy, not editing a growing if/else . In modern Java a strategy is usually just a functional interface plus a lambda.

An Observer (a.k.a. publish/subscribe) lets many listeners react when something happens, while the publisher knows nothing about them. Subscribe once, and every future publish() notifies you automatically. It's the heart of UI events, messaging systems, and notifications.

A builder only chains if each setter returns this . Fill in the blanks so the calls chain and the pizza prints correctly. Check your result against the expected output in the comments.

The most senior thing you can learn here is restraint . Every pattern trades extra indirection for flexibility. If you're not getting the flexibility, you're just paying the cost — more files, more layers, harder-to-follow code.

A good rule: write the simple version first. When you feel the same pain twice — a constructor that keeps growing, an if/else that keeps sprouting cases — that's the signal to refactor toward a pattern.

Time to fly solo. Build a tiny Decorator chain from scratch — only a comment outline is given. Follow the steps and match the expected output.

💡 Enum Singleton wins in Java — thread-safe, serialization-safe and reflection-proof with zero boilerplate.

💡 Builder + immutable object is a power combo: make every field final and settable only through the builder.

💡 Lambdas are strategies. A functional interface plus a lambda is the modern, boilerplate-free Strategy pattern — no separate class per algorithm.

💡 In interviews , knowing when not to use a pattern impresses more than reciting all 23.

You can now reach for the right blueprint by name: Singleton and Factory and Builder to create objects, Decorator and Adapter to compose them, and Strategy and Observer to wire up behaviour — plus the judgement to leave a pattern out when it doesn't earn its keep.

Next up: Interfaces, Abstraction & Best Practices — default methods, sealed interfaces, and interface-first design that makes these patterns even cleaner.

Practice quiz

Which is the safest Singleton idiom in Java?

  • A naive lazy 'if (instance == null) instance = new X();'
  • A static field set in a constructor
  • An enum with a single INSTANCE value
  • A new instance per call

Answer: An enum with a single INSTANCE value. The enum singleton is thread-safe, serialization-safe, and reflection-proof for free, with no locking code to get wrong.

In the lazy double-checked-locking Singleton, why is the instance field 'volatile'?

  • So writes are visible across threads (correct double-checked locking)
  • To make it faster
  • To allow multiple instances
  • It is optional and has no effect

Answer: So writes are visible across threads (correct double-checked locking). volatile ensures the write to instance is visible to other threads, which is required for double-checked locking to be safe.

What problem does the Factory Method pattern solve?

  • It guarantees one instance
  • It adds behaviour to an object
  • It broadcasts events
  • It hides which concrete class is created behind one call

Answer: It hides which concrete class is created behind one call. A factory centralises the 'new' keyword so callers ask for an interface type and get the right subtype back.

In a fluent Builder, why does each setter end with 'return this;'?

  • To validate the object
  • So the calls can be chained
  • To make the object mutable
  • To create a new builder each call

Answer: So the calls can be chained. Returning this lets you chain calls like .bun("brioche").cheese(true).build(). Forgetting it breaks the chain.

Where should a Builder put its validation?

  • Inside build(), so an invalid object can never be created
  • In every setter
  • In the constructor of the consumer
  • Nowhere — builders skip validation

Answer: Inside build(), so an invalid object can never be created. Validating inside build() means an invalid object is never constructed, so it can't exist anywhere in your program.

What is the key difference between Decorator and Adapter?

  • Decorator changes the interface; Adapter keeps it
  • They are the same pattern
  • Decorator adds behaviour with the same interface; Adapter translates one interface into another
  • Decorator is creational; Adapter is behavioral

Answer: Decorator adds behaviour with the same interface; Adapter translates one interface into another. Decorator enhances (coffee + milk is still a Coffee). Adapter bridges an incompatible class to the interface your code expects.

What does 'new Sugar(new Milk(new BasicCoffee())).describe()' print, given each wrapper appends ' + Milk' / ' + Sugar'?

  • Coffee + Sugar + Milk
  • Coffee + Milk + Sugar
  • Sugar + Milk + Coffee
  • Coffee

Answer: Coffee + Milk + Sugar. The innermost (BasicCoffee) builds 'Coffee', Milk appends ' + Milk', then Sugar appends ' + Sugar' -> 'Coffee + Milk + Sugar'.

What does the Strategy pattern let you do?

  • Guarantee one instance
  • Wrap an object to add behaviour
  • Notify many subscribers
  • Swap an interchangeable algorithm at runtime

Answer: Swap an interchangeable algorithm at runtime. Strategy selects an algorithm at runtime; in modern Java a functional interface plus a lambda is the idiomatic form.

In the Observer pattern, what is the relationship between publisher and subscribers?

  • The publisher knows each subscriber's class
  • The publisher knows nothing about subscribers; they react automatically on publish()
  • Subscribers poll the publisher
  • There can be only one subscriber

Answer: The publisher knows nothing about subscribers; they react automatically on publish(). Observer decouples the producer from consumers: subscribe once, and every publish() notifies all subscribers automatically.

When should you NOT introduce a design pattern?

  • When the same problem keeps recurring
  • When an algorithm must be swappable
  • When it adds indirection you don't need yet (e.g. a Builder for a two-field object)
  • When you need exactly one instance

Answer: When it adds indirection you don't need yet (e.g. a Builder for a two-field object). Patterns trade indirection for flexibility. If you aren't getting the flexibility, you're only paying the cost — keep it simple.