Prev Next

Java / Core Java 21 Design Patterns Interview questions

Last updated

1. What is a design pattern? 2. What are the types of design patterns? 3. What are the SOLID principles? 4. What is the Singleton pattern? 5. What is the Factory Method pattern? 6. What is the Abstract Factory pattern? 7. What is the Builder pattern? 8. What is the Prototype pattern? 9. What is the Adapter pattern? 10. What is the Decorator pattern? 11. What is the Facade pattern? 12. What is the Proxy pattern? 13. What is the Observer pattern? 14. What is the Strategy pattern? 15. What is the Command pattern? 16. What is the Template Method pattern? 17. What is the Iterator pattern? 18. What is the State pattern? 19. What is the Composite pattern? 20. What is the Flyweight pattern? 21. What is the Chain of Responsibility pattern? 22. What is the Visitor pattern? 23. What is the Mediator pattern? 24. What is the Memento pattern? 25. What is the Bridge pattern? 26. What is the Null Object pattern? 27. How do you use records for the Value Object pattern? 28. How do sealed interfaces help model closed hierarchies? 29. Why is an enum the preferred Singleton in Java? 30. How does double-checked locking work in a Singleton? 31. How does pattern matching for switch replace the Visitor pattern? 32. How do record patterns simplify nested data handling? 33. What is the difference between Factory Method and Abstract Factory? 34. What is the difference between Decorator and Proxy? 35. What is the difference between Strategy and State? 36. What is the difference between Adapter and Facade? 37. How do lambdas replace the Strategy pattern? 38. Where does the JDK use the Decorator pattern? 39. How can you implement the Builder pattern with records? 40. Why should you prefer composition over inheritance? 41. Why is java.util.Observable deprecated? 42. How does the Flow API implement the Observer pattern? 43. How does ServiceLoader implement the Service Provider pattern? 44. How does a dynamic proxy work in Java? 45. How do virtual threads change the thread pool pattern? 46. When would you avoid the Object Pool pattern? 47. How do you implement the Producer-Consumer pattern? 48. How does Dependency Injection work without a framework? 49. How do you break a Singleton and how do you prevent it? 50. How do you design an immutable class in Java 21?

1. What is a design pattern?

A design pattern is a proven, reusable solution to a design problem that keeps showing up in software. It is not finished code you paste in. It is a template that describes how classes and objects should collaborate, and you adapt it to your own codebase.

Patterns also give a team shared vocabulary. Saying "this is a Strategy" tells a reviewer the shape of the code without a diagram. The classic catalog comes from the 1994 Gang of Four book and lists 23 patterns.

In Java 21 some patterns get much smaller because of records, sealed types and lambdas. Reach for a pattern when it removes a real problem, not because it exists.

Take quiz
A design pattern is best described as:
A ready-made library added as a dependency
A reusable solution template for a recurring design problem
A compiler optimization applied to bytecode
How many patterns are in the original Gang of Four catalog?
12
23
42
7

2. What are the types of design patterns?

The Gang of Four catalog groups patterns into three categories based on the problem they solve.

Category Concern Examples
Creational How objects are created Singleton, Factory Method, Abstract Factory, Builder, Prototype
Structural How classes and objects are composed Adapter, Decorator, Facade, Proxy, Composite, Bridge, Flyweight
Behavioral How objects communicate and share responsibility Observer, Strategy, Command, State, Iterator, Visitor, Template Method

Beyond these, Java developers also use concurrency patterns such as Producer-Consumer, Thread Pool and Immutable Object, and architectural patterns such as Dependency Injection.

Take quiz
Which category does the Adapter pattern belong to?
Creational
Behavioral
Structural
Which of these is a creational pattern?
Prototype
Observer
Facade
Iterator

3. What are the SOLID principles?

SOLID is a set of five object-oriented design principles. Most design patterns exist to satisfy one or more of them.

Letter Principle Meaning
S Single Responsibility A class should have one reason to change
O Open/Closed Open for extension, closed for modification
L Liskov Substitution A subtype must be usable wherever its parent type is expected
I Interface Segregation Prefer small focused interfaces over one large one
D Dependency Inversion Depend on abstractions, not concrete classes

For example, Strategy supports Open/Closed because you add a new algorithm as a new class instead of editing a switch statement. Factory Method and Dependency Injection support Dependency Inversion.

Take quiz
The Open/Closed principle says a class should be:
Open for extension but closed for modification
Open for modification but closed for extension
Open to all callers and closed to subclasses
Which principle does Dependency Injection mainly support?
Single Responsibility
Liskov Substitution
Interface Segregation
Dependency Inversion

4. What is the Singleton pattern?

The Singleton pattern guarantees that a class has exactly one instance and gives a global access point to it. The JDK's Runtime.getRuntime() is a well-known example.

The cleanest lazy and thread-safe version without any locking is the initialization-on-demand holder idiom:

public final class AppConfig {
    private AppConfig() {}

    private static class Holder {
        static final AppConfig INSTANCE = new AppConfig();
    }

    public static AppConfig getInstance() {
        return Holder.INSTANCE;
    }
}

The JVM initializes Holder only when getInstance() first runs, and class initialization is guaranteed to happen once. Use Singletons sparingly because global state makes testing harder.

Take quiz
Which JDK class follows the Singleton pattern through a static accessor?
ArrayList
StringBuilder
LocalDate
Runtime
Why is the holder idiom thread-safe without synchronized?
Static methods are always synchronized
JVM class initialization runs once and is lazy on first access
The private constructor blocks other threads

5. What is the Factory Method pattern?

Factory Method moves object creation into a method, so the caller asks for an object by abstraction and never calls new on a concrete class. The method decides which implementation to return.

public interface Notifier { void send(String msg); }

public final class NotifierFactory {
    public static Notifier create(String channel) {
        return switch (channel) {
            case "EMAIL" -> new EmailNotifier();
            case "SMS"   -> new SmsNotifier();
            default -> throw new IllegalArgumentException(channel);
        };
    }
}

The JDK uses this often: List.of(), Calendar.getInstance() and NumberFormat.getInstance() all hide the concrete class from you.

Take quiz
Which call is a factory method in the JDK?
new ArrayList<>()
System.out.println()
List.of(1, 2, 3)
Math.max(1, 2)
What is the main benefit of Factory Method?
It guarantees only one instance exists
It makes every object immutable
The caller depends on an abstraction instead of a concrete class

6. What is the Abstract Factory pattern?

Abstract Factory provides an interface for creating a whole family of related objects without naming their concrete classes. The client holds one factory and everything it produces is guaranteed to match.

Think of a UI toolkit. A DarkThemeFactory creates DarkButton and DarkCheckbox, while a LightThemeFactory creates the light versions. Swapping one factory object changes the entire look.

interface UiFactory {
    Button createButton();
    Checkbox createCheckbox();
}
class DarkUiFactory implements UiFactory {
    public Button createButton() { return new DarkButton(); }
    public Checkbox createCheckbox() { return new DarkCheckbox(); }
}

In the JDK, DocumentBuilderFactory in javax.xml.parsers follows the same idea.

Take quiz
Abstract Factory is designed to create:
Families of related objects that belong together
One object with many optional fields
Copies of an existing object
Switching an entire UI from light to dark by replacing one factory object is an example of:
Abstract Factory
Singleton
Flyweight
Memento

7. What is the Builder pattern?

Builder separates the construction of a complex object from its final representation, letting you set fields step by step with readable method calls. It solves the telescoping constructor problem, where a class needs many constructors for optional parameters.

The JDK's HttpRequest is a good example:

HttpRequest request = HttpRequest.newBuilder()
        .uri(URI.create("https://example.com/api"))
        .header("Accept", "application/json")
        .timeout(Duration.ofSeconds(10))
        .GET()
        .build();

StringBuilder and Stream.builder() use the same idea. The builder can also validate the combination of fields in build() before returning the object.

Take quiz
Which problem does the Builder pattern mainly solve?
Sharing one instance across threads
Telescoping constructors with many optional parameters
Converting between incompatible interfaces
Which JDK API is created through a builder?
Math.abs()
Objects.equals()
Integer.parseInt()
HttpRequest.newBuilder()

8. What is the Prototype pattern?

Prototype creates new objects by copying an existing, already configured instance instead of building one from scratch. It helps when construction is expensive or the setup is complicated.

Java offers Object.clone() and Cloneable, but both are widely considered flawed: the copy is shallow by default, constructors are skipped and the contract is awkward. A copy constructor is the safer option:

public class Report {
    private final String title;
    private final List<String> rows;

    public Report(Report other) {
        this.title = other.title;
        this.rows = new ArrayList<>(other.rows);
    }
}

Here the list is copied explicitly, so changes to one report never leak into the other.

Take quiz
Why is Object.clone() discouraged in modern Java?
It was removed in Java 21
It only works on records
It makes shallow copies by default and bypasses constructors
Prototype is most useful when:
You need exactly one instance in the JVM
You want to hide a complex subsystem
Copying a configured instance is cheaper than building a new one

9. What is the Adapter pattern?

Adapter converts the interface of an existing class into the interface your client expects, so two incompatible classes can work together without either being changed.

interface PaymentGateway { void pay(double amount); }

class LegacyBank { void makeTransfer(int cents) { /* ... */ } }

class BankAdapter implements PaymentGateway {
    private final LegacyBank bank;
    BankAdapter(LegacyBank bank) { this.bank = bank; }

    public void pay(double amount) {
        bank.makeTransfer((int) Math.round(amount * 100));
    }
}

The JDK has several adapters. InputStreamReader adapts a byte stream to a character Reader, and Arrays.asList() adapts an array to the List interface.

Take quiz
InputStreamReader acts as an Adapter between:
Byte streams and character streams
Threads and processes
Strings and arrays
What is the goal of the Adapter pattern?
Make incompatible interfaces work together without changing either
Add responsibilities to an object at runtime
Delay creation of an expensive object

10. What is the Decorator pattern?

Decorator adds behavior to an object dynamically by wrapping it in another object that implements the same interface. Each wrapper does its extra work and then delegates to the wrapped object.

interface Coffee { double cost(); }

class Espresso implements Coffee {
    public double cost() { return 2.0; }
}

class MilkDecorator implements Coffee {
    private final Coffee base;
    MilkDecorator(Coffee base) { this.base = base; }
    public double cost() { return base.cost() + 0.5; }
}

Coffee order = new MilkDecorator(new Espresso());

Because wrappers can be stacked in any order, you avoid a subclass for every combination of features.

Take quiz
Which class in java.io is a Decorator?
File
BufferedReader
Path
Console
What is the key structural trait of a Decorator?
It extends a concrete class and overrides everything
It implements the same interface as the object it wraps and delegates to it
It creates the wrapped object internally using reflection

11. What is the Facade pattern?

Facade offers one simple, unified interface in front of a complicated subsystem. Clients call the facade instead of coordinating many classes themselves.

class OrderFacade {
    private final Inventory inventory;
    private final Payment payment;
    private final Shipping shipping;

    OrderFacade(Inventory i, Payment p, Shipping s) {
        this.inventory = i; this.payment = p; this.shipping = s;
    }

    void placeOrder(String sku, int qty) {
        inventory.reserve(sku, qty);
        payment.charge(sku, qty);
        shipping.schedule(sku, qty);
    }
}

The subsystem is not hidden forever. Advanced users can still call Inventory directly. In the JDK, Files.readString() is a small facade over streams, charsets and buffers.

Take quiz
What does a Facade provide?
A way to clone objects cheaply
A single global instance
A simplified entry point to a complex subsystem
After adding a Facade, can clients still use the subsystem classes directly?
No, the subsystem becomes private automatically
Only if the subsystem classes are made records
Yes, the facade only adds a convenient entry point

12. What is the Proxy pattern?

Proxy is a stand-in object that controls access to another object. It implements the same interface as the real object, so the client cannot tell the difference.

Proxy type Purpose
Virtual Create an expensive object only on first use
Protection Check permissions before forwarding a call
Remote Represent an object living in another JVM or server
Caching / logging Add cross-cutting behavior around calls

sequenceDiagram
    participant Client
    participant Proxy
    participant RealService
    Client->>Proxy: request()
    Proxy->>Proxy: check access or cache
    Proxy->>RealService: request()
    RealService-->>Proxy: result
    Proxy-->>Client: result

Java also supports runtime proxies through java.lang.reflect.Proxy.

Take quiz
A proxy that delays creating an expensive object until it is first used is called a:
Virtual proxy
Protection proxy
Remote proxy
Why can a client use a proxy without knowing it?
The proxy implements the same interface as the real object
The proxy extends Object only
The proxy is always a singleton

13. What is the Observer pattern?

Observer defines a one-to-many relationship. When the subject changes state, every registered observer is notified automatically, and the subject does not need to know what they do.

class PriceFeed {
    private final List<Consumer<Double>> listeners = new CopyOnWriteArrayList<>();

    void subscribe(Consumer<Double> listener) { listeners.add(listener); }

    void publish(double price) {
        listeners.forEach(l -> l.accept(price));
    }
}

feed.subscribe(p -> System.out.println("Price: " + p));

CopyOnWriteArrayList lets observers subscribe or unsubscribe during a notification without a ConcurrentModificationException. Swing listeners and PropertyChangeListener are classic JDK uses.

Take quiz
Why use CopyOnWriteArrayList for the observer list?
It sorts observers by priority
Observers can be added or removed during notification safely
It makes observers immutable
Which JDK feature is a classic use of Observer?
String.intern()
Swing event listeners
Math.random()
Arrays.sort()

14. What is the Strategy pattern?

Strategy puts a family of interchangeable algorithms behind one interface, so the algorithm can be chosen or swapped at runtime. It replaces long if/else or switch chains that pick behavior.

The JDK's Comparator is the textbook example. sort() stays the same while you hand it different comparison strategies:

List<String> names = new ArrayList<>(List.of("Priya", "Al", "Marcus"));
names.sort(Comparator.comparing(String::length));
names.sort(Comparator.naturalOrder());

Adding a new algorithm means adding a new implementation, not editing existing code, which fits the Open/Closed principle.

Take quiz
Passing a Comparator to List.sort() is an example of:
Singleton
Strategy
Prototype
Facade
Strategy mainly removes:
Null checks on return values
Checked exceptions
Conditional chains that select an algorithm

15. What is the Command pattern?

Command wraps a request as an object. Because the action is now a value, you can queue it, log it, schedule it or undo it.

interface Command { void execute(); void undo(); }

class AppendText implements Command {
    private final StringBuilder doc; private final String text;
    AppendText(StringBuilder doc, String text) { this.doc = doc; this.text = text; }
    public void execute() { doc.append(text); }
    public void undo() { doc.setLength(doc.length() - text.length()); }
}

Deque<Command> history = new ArrayDeque<>();
Command c = new AppendText(doc, "Hello");
c.execute();
history.push(c);
history.pop().undo();

Runnable and Callable are lightweight commands: an action packaged as an object that an executor runs later.

Take quiz
Which capability does the Command pattern make possible?
Undo, redo and queuing of operations
Automatic memory management
Compile-time exhaustive checks
Why does Runnable resemble a Command?
It packages an action as an object that can be passed around and run later
It forces a single instance per JVM
It converts one interface into another

16. What is the Template Method pattern?

Template Method defines the skeleton of an algorithm in a base class and lets subclasses fill in specific steps. The overall order stays fixed, and only the parts that vary are overridden.

abstract class DataExporter {
    public final void export() {
        open();
        write();
        close();
    }
    protected void open()  { System.out.println("Opening"); }
    protected abstract void write();
    protected void close() { System.out.println("Closing"); }
}

The template method is final so subclasses cannot reorder the steps. In the JDK, AbstractList builds most of List on top of get() and size(). Unlike Strategy, which uses composition, this pattern relies on inheritance.

Take quiz
Why is the template method usually declared final?
So it can be called from static code
So subclasses cannot change the order of the algorithm steps
So it runs faster on the JVM
What is the key difference between Template Method and Strategy?
Template Method works only with records
Template Method uses inheritance while Strategy uses composition
Strategy needs a singleton while Template Method does not

17. What is the Iterator pattern?

Iterator gives sequential access to the elements of a collection without exposing how the collection stores them. In Java the pattern is built in through Iterator and Iterable, and the enhanced for loop uses it behind the scenes.

record Range(int start, int end) implements Iterable<Integer> {
    public Iterator<Integer> iterator() {
        return new Iterator<>() {
            private int next = start;
            public boolean hasNext() { return next < end; }
            public Integer next() { return next++; }
        };
    }
}

for (int i : new Range(1, 4)) System.out.println(i);

Most JDK collection iterators are fail-fast: if the collection is structurally modified during iteration, they throw ConcurrentModificationException.

Take quiz
What must a class implement to be used in an enhanced for loop?
Comparable
Cloneable
Iterable
Serializable
What happens when an ArrayList is structurally modified during a for-each loop?
The loop silently skips the new element
The list is locked until iteration ends
A fail-fast iterator throws ConcurrentModificationException

18. What is the State pattern?

State lets an object change its behavior when its internal state changes, by moving state-specific logic into separate state types. It replaces scattered if (status == ...) checks.

stateDiagram-v2
    [*] --> NEW
    NEW --> PAID: pay()
    PAID --> SHIPPED: ship()
    SHIPPED --> [*]

A compact Java version uses an enum where each constant owns its transition:

enum OrderState {
    NEW     { OrderState next() { return PAID; } },
    PAID    { OrderState next() { return SHIPPED; } },
    SHIPPED { OrderState next() { return this; } };

    abstract OrderState next();
}

For states that carry data, use a sealed interface with one record per state.

Take quiz
State pattern mainly eliminates:
Scattered conditionals that check a status field
Constructor overloading
Memory leaks from listeners
In the State pattern, what usually triggers the move to the next state?
The current state object decides the transition
A background garbage collection cycle
The compiler at build time

19. What is the Composite pattern?

Composite arranges objects in a tree and lets clients treat a single object and a group of objects through the same interface. A file system is the usual example: a folder contains files and other folders.

sealed interface Node permits FileNode, Folder {
    long size();
}
record FileNode(String name, long bytes) implements Node {
    public long size() { return bytes; }
}
record Folder(String name, List<Node> children) implements Node {
    public long size() {
        return children.stream().mapToLong(Node::size).sum();
    }
}

Calling size() on a folder recurses through every child, and the caller never checks whether a node is a leaf or a group.

Take quiz
What does Composite let a client do?
Share one instance globally
Treat a leaf and a group of objects uniformly
Wrap an object to add logging
How does Folder.size() produce a total in the example?
It reads a cached static counter
It sums size() across its children recursively
It converts every child to a string first

20. What is the Flyweight pattern?

Flyweight saves memory by sharing many small, immutable objects instead of creating duplicates. The shared part is intrinsic state (stored inside the object). Anything that differs per use is extrinsic state and is passed in by the caller.

The JDK applies it in several places. Integer.valueOf() caches values from -128 to 127, and the String pool reuses identical literals.

Integer a = Integer.valueOf(100);
Integer b = Integer.valueOf(100);
System.out.println(a == b);   // true, same cached object

Integer c = Integer.valueOf(1000);
Integer d = Integer.valueOf(1000);
System.out.println(c == d);   // false, outside the cache

Always compare boxed values with equals(), never ==.

Take quiz
What is the default range of the Integer cache?
0 to 255
-1024 to 1023
0 to 100
-128 to 127
In Flyweight, extrinsic state is:
The data stored inside every shared object
The class loader used to create the object
Data supplied by the caller on each use and not stored in the shared object

21. What is the Chain of Responsibility pattern?

Chain of Responsibility passes a request along a series of handlers. Each handler either processes it or forwards it to the next one, so the sender does not need to know which handler will act.

flowchart LR
    R[Request] --> A["Auth handler"]
    A -->|passed| L["Rate limit handler"]
    L -->|passed| V["Validation handler"]
    V --> S["Business logic"]
    A -->|rejected| X[Stop]

Servlet filters and the parent handler lookup in java.util.logging follow this idea. A simple pipeline variant looks like this:

List<Predicate<Request>> chain = List.of(auth, rateLimit, validator);
boolean ok = chain.stream().allMatch(h -> h.test(request));

Take quiz
What is the main advantage of Chain of Responsibility?
The sender is decoupled from the handler that processes the request
It guarantees only one handler class exists
It removes the need for interfaces
In a filter chain, what happens when a handler rejects the request?
Processing stops and later handlers are skipped
Every handler runs anyway
The request restarts from the first handler

22. What is the Visitor pattern?

Visitor lets you add new operations to a class hierarchy without modifying the classes. It works through double dispatch: the element calls visitor.visit(this), so the right overload runs for both the visitor and the element type.

interface Shape { <R> R accept(ShapeVisitor<R> v); }

interface ShapeVisitor<R> {
    R visitCircle(Circle c);
    R visitSquare(Square s);
}

record Circle(double r) implements Shape {
    public <R> R accept(ShapeVisitor<R> v) { return v.visitCircle(this); }
}

The weak spot is that adding a new element type forces you to update every visitor. In Java 21, sealed types with pattern matching for switch often replace the whole ceremony.

Take quiz
Visitor relies on which technique to select the right method?
Reflection only
Double dispatch
Operator overloading
Static imports
What is the main weakness of the Visitor pattern?
It cannot be used with interfaces
Adding a new element type requires changing every visitor
It only works for two element types

23. What is the Mediator pattern?

Mediator puts one object in the middle to coordinate how a group of objects communicate. Instead of every component holding references to every other component, each one talks only to the mediator.

class ChatRoom {
    private final List<User> users = new ArrayList<>();
    void join(User u) { users.add(u); }
    void send(User from, String msg) {
        for (User u : users) {
            if (u != from) u.receive(from.name() + ": " + msg);
        }
    }
}

This turns a tangle of many-to-many links into a simple star shape. Dialog boxes, chat rooms and event buses use it. The risk is that the mediator slowly grows into a god object holding too much logic.

Take quiz
What problem does Mediator solve?
Slow object creation
Duplicate objects in memory
Tight many-to-many dependencies between components
What is a common risk of the Mediator pattern?
It makes every component immutable
It prevents objects from being garbage collected
The mediator becomes an oversized god object

24. What is the Memento pattern?

Memento captures an object's internal state in a snapshot so it can be restored later, without exposing the object's internals. It powers undo, checkpoints and rollback.

class Editor {
    private String text = "";

    record Snapshot(String text) {}

    void type(String s)        { text += s; }
    Snapshot save()            { return new Snapshot(text); }
    void restore(Snapshot s)   { this.text = s.text(); }
}

A record is a natural fit for the snapshot because it is immutable, so a saved state cannot be changed by accident. A caretaker, such as a Deque, keeps the history.

Take quiz
Memento is typically used to implement:
Undo and state checkpoints
Lazy loading of objects
Thread pooling
Why is a record a good choice for a Memento snapshot?
It is immutable, so saved state cannot be altered
It automatically persists to disk
It can extend the originator class

25. What is the Bridge pattern?

Bridge separates an abstraction from its implementation so the two can vary independently. It prevents the class explosion you get when two dimensions of variation are forced into one inheritance tree.

interface MessageSender { void deliver(String text); }
class EmailSender implements MessageSender { public void deliver(String t) { /* ... */ } }
class SmsSender   implements MessageSender { public void deliver(String t) { /* ... */ } }

abstract class Notification {
    protected final MessageSender sender;
    Notification(MessageSender sender) { this.sender = sender; }
    abstract void notifyUser(String text);
}
class Alert extends Notification {
    Alert(MessageSender s) { super(s); }
    void notifyUser(String t) { sender.deliver("ALERT: " + t); }
}

With 3 notification types and 3 channels you write 3 + 3 classes instead of 9. JDBC is the best-known real example: the Connection API stays fixed while each vendor supplies its own driver.

Take quiz
What problem does Bridge avoid?
Race conditions in static fields
Class explosion from combining two independent dimensions of variation
Shallow copying of arrays
Why is JDBC considered an example of Bridge?
It creates only one connection per JVM
The Connection API stays stable while vendor drivers vary
It wraps every driver in a decorator

26. What is the Null Object pattern?

Null Object replaces null with a real object that does nothing, or returns neutral values. Callers skip the null check because the object always responds safely.

OutputStream out = verbose
        ? System.out
        : OutputStream.nullOutputStream();

out.write("debug info".getBytes());   // silently discarded

The JDK uses it in several places: OutputStream.nullOutputStream(), Collections.emptyList() and Collections.emptyIterator(). For return values that might be absent, Optional is the standard alternative, though it should not replace collections that can simply be empty.

Take quiz
What does the Null Object pattern remove from calling code?
Constructor calls
Interface implementations
Repeated null checks
Why return Collections.emptyList() instead of null from a method?
It makes the list resizable
It forces callers to catch an exception
The caller can loop over it safely without a null check

27. How do you use records for the Value Object pattern?

A Value Object is defined by its data, not its identity, and should be immutable. A Java record gives you this almost for free: final fields, accessors, and generated equals(), hashCode() and toString().

public record Money(BigDecimal amount, String currency) {
    public Money {
        Objects.requireNonNull(amount);
        Objects.requireNonNull(currency);
        if (amount.signum() < 0) {
            throw new IllegalArgumentException("negative amount");
        }
    }

    public Money plus(Money other) {
        return new Money(amount.add(other.amount), currency);
    }
}

The compact constructor validates input. Records are only shallowly immutable, so if a component is a List, copy it with List.copyOf() inside the constructor.

Take quiz
Which members does a record generate automatically?
Accessors, equals, hashCode and toString
Setters and a no-arg constructor
A Builder and a clone method
A record has a List component. How do you keep it truly immutable?
Copy it with List.copyOf() in the compact constructor
Mark the record as sealed
Declare the record as abstract

28. How do sealed interfaces help model closed hierarchies?

A sealed type lists exactly which classes may implement or extend it using permits. The compiler then knows the full set of subtypes, which makes switch statements exhaustive without a default branch.

sealed interface PaymentMethod permits Card, Wallet, BankTransfer {}
record Card(String number) implements PaymentMethod {}
record Wallet(String id) implements PaymentMethod {}
record BankTransfer(String iban) implements PaymentMethod {}

String describe(PaymentMethod p) {
    return switch (p) {
        case Card c         -> "Card " + c.number();
        case Wallet w       -> "Wallet " + w.id();
        case BankTransfer b -> "Bank " + b.iban();
    };
}

Each permitted subtype must be final, sealed or non-sealed. If you add a fourth payment type later, every switch that misses it fails to compile, which is exactly the safety you want.

Take quiz
Each permitted subclass of a sealed type must be declared as:
abstract, static or volatile
final, sealed or non-sealed
public, private or protected only
What is the benefit of exhaustive switch over a sealed interface?
The switch runs in constant time
Adding a new subtype causes compile errors where it is not handled
A default branch is mandatory

29. Why is an enum the preferred Singleton in Java?

An enum with a single constant is considered the most robust Singleton because the JVM itself enforces three guarantees that you would otherwise code by hand.

  1. Thread safety: enum constants are created during class initialization, which runs once.
  2. Serialization safety: deserializing an enum returns the existing constant by name, never a new instance.
  3. Reflection safety: Constructor.newInstance() throws IllegalArgumentException for enum types.
public enum Registry {
    INSTANCE;

    private final Map<String, String> data = new ConcurrentHashMap<>();

    public void put(String k, String v) { data.put(k, v); }
    public String get(String k)         { return data.get(k); }
}

The limits are that an enum cannot extend another class and it initializes eagerly when first referenced.

Take quiz
Why can reflection not create a second enum Singleton instance?
Enum constructors are always public
Reflection is disabled inside enums
Constructor.newInstance() rejects enum types with an exception
What is a limitation of the enum Singleton?
It is not thread safe
It cannot hold fields
It cannot extend another class

30. How does double-checked locking work in a Singleton?

Double-checked locking avoids paying for synchronization on every call. The code checks for null without a lock, and only enters the synchronized block when the instance may not exist yet.

public class Cache {
    private static volatile Cache instance;

    private Cache() {}

    public static Cache get() {
        Cache local = instance;
        if (local == null) {
            synchronized (Cache.class) {
                local = instance;
                if (local == null) {
                    instance = local = new Cache();
                }
            }
        }
        return local;
    }
}

The second check inside the lock matters because another thread may have created the object while this one waited. The volatile keyword is mandatory: without it the reference can be published before the constructor finishes, and another thread could see a half-built object.

Take quiz
Why must the instance field be volatile in double-checked locking?
To prevent another thread from seeing a partially constructed object
To make the constructor run faster
To allow serialization of the singleton
Why is there a second null check inside the synchronized block?
Another thread may have created the instance while this one waited for the lock
The first check can throw an exception
The JVM clears static fields periodically

31. How does pattern matching for switch replace the Visitor pattern?

With a sealed hierarchy and pattern matching for switch (final in Java 21), you can add an operation as a plain method with a switch. The accept() and visitX() methods disappear.

sealed interface Shape permits Circle, Square {}
record Circle(double r) implements Shape {}
record Square(double side) implements Shape {}

static double area(Shape s) {
    return switch (s) {
        case Circle c -> Math.PI * c.r() * c.r();
        case Square q -> q.side() * q.side();
    };
}

The trade-offs are the same as in Visitor: adding an operation is easy, and adding a type is a compile error in every switch that misses it. Visitor still makes sense when the hierarchy is not sealed or lives in a library you cannot change.

Take quiz
Which feature lets the compiler verify a switch covers every subtype?
The final keyword on methods
A sealed hierarchy
Static nested classes
Which Visitor boilerplate disappears with pattern matching for switch?
The interface declaration
The accept() and visit methods used for double dispatch
Every record component

32. How do record patterns simplify nested data handling?

A record pattern checks the type and pulls out the components in one step, even for nested records. It is final in Java 21 (JEP 440) and works in both instanceof and switch.

record Point(int x, int y) {}
record Line(Point from, Point to) {}

static double length(Object o) {
    if (o instanceof Line(Point(var x1, var y1), Point(var x2, var y2))) {
        return Math.hypot(x2 - x1, y2 - y1);
    }
    return 0;
}

Without it you would cast, call from(), call x(), and repeat for each level. In a switch you can add a when guard, for example case Line(Point p, Point q) when p.equals(q) -> 0. The var types are inferred from the record components.

Take quiz
In which Java version did record patterns become a final feature?
Java 14
Java 17
Java 23
Java 21
In a record pattern, the type of 'var x1' is:
Always Object
Always int regardless of the record
Inferred from the matching record component

33. What is the difference between Factory Method and Abstract Factory?

Both hide concrete classes from the caller, but they differ in scope and structure.

Aspect Factory Method Abstract Factory
Creates One product A family of related products
Mechanism A single method, often overridden or static A separate factory object with several methods
Consistency Not enforced across products Guarantees products belong together
Adding a product type Add a new method or subclass Change the factory interface and all factories
JDK example List.of() DocumentBuilderFactory

Abstract Factory is often built from several Factory Methods. If you only create one kind of object, Factory Method is enough.

Take quiz
Which pattern guarantees that created products belong to the same family?
Factory Method
Prototype
Abstract Factory
Singleton
How is Factory Method typically structured?
A single creation method that may be overridden or static
A factory object exposing many creation methods
A copy constructor on every product

34. What is the difference between Decorator and Proxy?

Structurally they look alike, since both wrap an object and share its interface. The difference is intent.

Aspect Decorator Proxy
Intent Add new behavior or responsibilities Control access to the real object
Wrapped object Passed in by the client Often created or managed by the proxy itself
Stacking Commonly stacked several layers deep Usually a single layer
Lifecycle Client controls it Proxy may defer or skip creating the real object
Example BufferedReader over a Reader Lazy-loading or permission-checking wrapper

A quick test: if the wrapper makes the object do more, it is a Decorator. If it decides whether or when the object is used, it is a Proxy.

Take quiz
Which pattern is mainly about controlling access to the real object?
Decorator
Proxy
Composite
Bridge
A wrapper that is commonly stacked several layers deep is typically a:
Decorator
Proxy
Singleton
Flyweight

35. What is the difference between Strategy and State?

Both delegate behavior to a replaceable object, and their class diagrams look almost identical. The difference is who changes the object and why.

Aspect Strategy State
Purpose Choose how a task is done Change behavior as the object's status changes
Who switches The client, from outside The states themselves or the context, from inside
Awareness Strategies know nothing about each other States usually know the next state
Typical lifetime Often set once and kept Changes repeatedly during the object's life
Example Comparator passed to sort() Order moving from NEW to PAID to SHIPPED

If the choice is a configuration decision, it is Strategy. If the object walks through a life cycle, it is State.

Take quiz
Who normally switches the active object in the State pattern?
The client choosing an algorithm from outside
The garbage collector
The states or the context, based on internal transitions
An order moving from NEW to PAID to SHIPPED is best modeled as:
Strategy
Flyweight
Prototype
State

36. What is the difference between Adapter and Facade?

Both sit between a client and existing code, but they solve different problems.

Aspect Adapter Facade
Goal Make an existing interface match the one the client expects Provide a new, simpler interface
Wraps Usually one class Many classes of a subsystem
Interface Dictated by the client Designed fresh for convenience
Typical trigger Integrating a legacy or third-party API A subsystem is too complex to call directly
JDK example InputStreamReader Files.readString()

Adapter fixes a mismatch, and Facade reduces complexity. A facade may use several adapters inside it.

Take quiz
Which pattern exists mainly to convert one interface into the one a client expects?
Facade
Mediator
Observer
Adapter
A class that wraps several subsystem classes behind one convenient method is a:
Adapter
Decorator
Facade
Memento

37. How do lambdas replace the Strategy pattern?

When the strategy interface has a single abstract method (a functional interface), a lambda or method reference can stand in for a whole class. You skip the named implementations.

Map<String, DoubleUnaryOperator> discounts = Map.of(
    "NONE",     price -> price,
    "SEASONAL", price -> price * 0.90,
    "VIP",      price -> price * 0.80
);

double total = discounts.get(code).applyAsDouble(price);

This is shorter and keeps the algorithms next to the code that selects them. Keep real classes when the strategy has state, needs several methods, or must be named for testing, logging or dependency injection. A lambda is a good default, not a rule.

Take quiz
A lambda can replace a Strategy class when the interface is:
A sealed interface
A functional interface with one abstract method
An interface with five default methods
When is a full Strategy class still better than a lambda?
When the algorithm is a one-line calculation
When the strategy holds state or needs several methods
When the code runs on a single thread

38. Where does the JDK use the Decorator pattern?

The most visible example is java.io. Each stream or reader wraps another one and adds a feature:

try (BufferedReader reader = new BufferedReader(
        new InputStreamReader(
            new FileInputStream("data.txt"), StandardCharsets.UTF_8))) {
    System.out.println(reader.readLine());
}

  • FileInputStream reads raw bytes from a file.
  • InputStreamReader decodes bytes into characters.
  • BufferedReader adds buffering and readLine().

The Collections framework has wrappers too: Collections.unmodifiableList(), synchronizedList() and checkedList() each wrap a list and add one behavior. Files.newBufferedReader() is a facade that builds this chain for you.

Take quiz
In the chain BufferedReader over InputStreamReader over FileInputStream, what does BufferedReader add?
Character decoding from bytes
File system access
Buffering and the readLine() method
Which method wraps a list to add behavior in the Collections framework?
Collections.unmodifiableList()
List.copyOf() only
Arrays.fill()
Objects.hash()

39. How can you implement the Builder pattern with records?

A record's canonical constructor takes every component, which gets hard to read with many parameters. A nested Builder gives you named steps while the record keeps validation in its compact constructor.

public record User(String name, String email, int age, boolean active) {
    public User {
        Objects.requireNonNull(name);
        if (age < 0) throw new IllegalArgumentException("age");
    }

    public static Builder builder() { return new Builder(); }

    public static final class Builder {
        private String name;
        private String email = "";
        private int age;
        private boolean active = true;

        public Builder name(String v)    { this.name = v; return this; }
        public Builder email(String v)   { this.email = v; return this; }
        public Builder age(int v)        { this.age = v; return this; }
        public Builder active(boolean v) { this.active = v; return this; }
        public User build() { return new User(name, email, age, active); }
    }
}

Because build() goes through the canonical constructor, the builder can never produce an invalid record. For small changes, a "wither" method such as withEmail() that returns a new record is often enough.

Take quiz
Where should validation live when using a Builder with a record?
In the record's compact constructor
Only inside the Builder setters
In a static initializer block
Why does build() calling the canonical constructor matter?
It ensures the builder cannot produce an invalid record
It makes the record mutable
It lets the record extend another class

40. Why should you prefer composition over inheritance?

Inheritance ties a subclass to the implementation details of its parent. When the parent changes, subclasses can break, which is called the fragile base class problem. Composition keeps a reference to another object and delegates, so you depend only on its public interface.

A classic case from Effective Java: a subclass of HashSet that counts added elements by overriding add() and addAll(). Because HashSet.addAll() internally calls add(), every element is counted twice.

class CountingSet<E> {
    private final Set<E> delegate = new HashSet<>();
    private int added;

    public boolean add(E e) { added++; return delegate.add(e); }
    public boolean addAll(Collection<? extends E> c) {
        added += c.size();
        return delegate.addAll(c);
    }
    public int added() { return added; }
}

Composition also lets you swap the delegate at runtime. Use inheritance only for a true is-a relationship, and ideally from a class designed for extension.

Take quiz
What is the fragile base class problem?
The parent class cannot be instantiated
Subclasses break when the parent's internal implementation changes
Subclasses cannot override public methods
Why does a HashSet subclass overriding add() and addAll() double count?
add() is final in HashSet
HashSet.addAll() calls add() internally
HashSet stores duplicates by default

41. Why is java.util.Observable deprecated?

Observable and Observer were deprecated in Java 9 because the model is too limited for real use.

  • Observable is a class, so a type that already extends something else cannot reuse it.
  • setChanged() is protected, so you cannot compose it into another object.
  • The order in which observers are notified is unspecified.
  • Events are plain Object values, which are not type safe or serializable by design.
  • State changes and notifications are not guaranteed to match one for one.

Better options are java.beans.PropertyChangeSupport for bean properties, the java.util.concurrent.Flow API for asynchronous streams with backpressure, or a small typed listener list like List<Consumer<Event>>.

Take quiz
Why is Observable awkward to reuse in many designs?
It is a final interface
It cannot hold more than one observer
It is a class, so it consumes your single superclass slot
Which JDK type is a modern alternative for asynchronous observers with backpressure?
java.util.Observable
java.util.concurrent.Flow
java.lang.Runtime
java.io.Console

42. How does the Flow API implement the Observer pattern?

java.util.concurrent.Flow (Java 9) is a reactive-streams style Observer with one major addition: backpressure. It defines four interfaces: Publisher, Subscriber, Subscription and Processor.

sequenceDiagram
    participant S as Subscriber
    participant P as Publisher
    S->>P: subscribe(subscriber)
    P-->>S: onSubscribe(subscription)
    S->>P: subscription.request(1)
    P-->>S: onNext(item)
    S->>P: subscription.request(1)
    P-->>S: onComplete()
class PrintSubscriber implements Flow.Subscriber<String> {
    private Flow.Subscription sub;
    public void onSubscribe(Flow.Subscription s) { sub = s; s.request(1); }
    public void onNext(String item) { System.out.println(item); sub.request(1); }
    public void onError(Throwable t) { t.printStackTrace(); }
    public void onComplete() { System.out.println("done"); }
}

try (var pub = new SubmissionPublisher<String>()) {
    pub.subscribe(new PrintSubscriber());
    pub.submit("hello");
}

The subscriber asks for items with request(n), so a slow consumer is never flooded. SubmissionPublisher is the ready-made JDK implementation.

Take quiz
How does a Flow Subscriber control the rate of incoming items?
By calling Subscription.request(n)
By sleeping inside onNext only
By closing the Publisher
Which JDK class is a ready-made Flow.Publisher?
SubmissionPublisher
BlockingQueue
CopyOnWriteArrayList
StringBuilder

43. How does ServiceLoader implement the Service Provider pattern?

ServiceLoader finds implementations of an interface at runtime, so the application depends only on the interface, not on any concrete provider. JDBC drivers and charset providers are discovered this way.

With the module system, the contract is declared in module-info.java:

// consumer module
module app {
    uses com.acme.spi.Codec;
}

// provider module
module codec.json {
    provides com.acme.spi.Codec with com.acme.json.JsonCodec;
}

// consumer code
ServiceLoader<Codec> loader = ServiceLoader.load(Codec.class);
for (Codec c : loader) {
    System.out.println(c.name());
}

On the classpath, providers are listed in META-INF/services/com.acme.spi.Codec instead. Providers are instantiated lazily while you iterate, and you can drop a new JAR on the path without recompiling the consumer.

Take quiz
Which module-info directive does a consumer use to request a service?
uses
provides
exports
requires static
When does ServiceLoader create provider instances?
All at JVM startup
Lazily, as you iterate over the loader
Only during compilation

44. How does a dynamic proxy work in Java?

java.lang.reflect.Proxy creates a proxy class at runtime that implements the interfaces you give it. Every method call on the proxy is routed to a single InvocationHandler.invoke() method.

sequenceDiagram
    participant C as Client
    participant X as Proxy instance
    participant H as InvocationHandler
    participant R as Real object
    C->>X: placeOrder(item)
    X->>H: invoke(proxy, method, args)
    H->>R: method.invoke(real, args)
    R-->>H: result
    H-->>C: result
OrderService real = new OrderServiceImpl();
OrderService proxy = (OrderService) Proxy.newProxyInstance(
    OrderService.class.getClassLoader(),
    new Class<?>[] { OrderService.class },
    (p, method, args) -> {
        long start = System.nanoTime();
        try {
            return method.invoke(real, args);
        } finally {
            System.out.println(method.getName() + " took "
                + (System.nanoTime() - start) + " ns");
        }
    });

Two limits to remember: the JDK proxy works only with interfaces, and exceptions from the real method arrive wrapped in InvocationTargetException, so unwrap them. Frameworks use this for logging, transactions and mocking.

Take quiz
What can java.lang.reflect.Proxy proxy by default?
Any final class
Only enums
Only records
Interfaces only
Where do all method calls on a JDK dynamic proxy end up?
In the Proxy constructor
In Class.forName()
In InvocationHandler.invoke()

45. How do virtual threads change the thread pool pattern?

The Thread Pool pattern exists because platform threads are expensive, so you create a few and reuse them. Virtual threads (final in Java 21) are cheap, lightweight and scheduled by the JVM, so pooling them is pointless and can hurt.

The new rule is one virtual thread per task:

try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    for (String url : urls) {
        executor.submit(() -> fetch(url));
    }
}   // close() waits for submitted tasks

The pool was also acting as a limit on concurrency. That job now moves to a Semaphore placed around the scarce resource, such as a database or a rate-limited API:

Semaphore permits = new Semaphore(20);
permits.acquire();
try { callDatabase(); } finally { permits.release(); }

Virtual threads suit blocking, I/O-heavy work. They give no benefit for CPU-bound tasks.

Take quiz
What is the recommended way to run tasks on virtual threads?
Create one virtual thread per task without pooling
Reuse a fixed pool of 8 virtual threads
Share one virtual thread among all tasks
How do you limit concurrent access to a scarce resource when not pooling threads?
Guard it with a Semaphore
Make the method static
Call System.gc() before each task

46. When would you avoid the Object Pool pattern?

An Object Pool reuses expensive objects instead of creating new ones. It pays off for resources that are truly costly or limited, such as database connections. For ordinary objects it usually makes things worse.

Pool it Do not pool it
Database or network connections Small short-lived objects like DTOs or strings
Native or direct buffers Virtual threads, which are cheap by design
Objects limited by licence or hardware Anything JVM allocation handles quickly

Modern JVMs allocate small objects very cheaply and the garbage collector handles short-lived ones well. A pool adds locking, risks leaking stale state from one user to the next, and keeps memory alive that the GC would have freed.

Also be careful with ThreadLocal caches. With virtual threads you may have millions of threads, so each would hold its own copy of an expensive object.

Take quiz
For which resource is an Object Pool most appropriate?
Small DTO objects
Database connections
Short-lived strings
Virtual threads
What risk does pooling ordinary small objects introduce?
It forces the use of reflection
Stale state can leak between users and contention can slow things down
It disables the garbage collector

47. How do you implement the Producer-Consumer pattern?

Use a BlockingQueue between the two sides. Producers call put(), which blocks when the queue is full. Consumers call take(), which blocks when the queue is empty. The queue handles all the locking.

BlockingQueue<String> queue = new ArrayBlockingQueue<>(100);

Thread.ofVirtual().start(() -> {
    try {
        queue.put("job-1");
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }
});

Thread.ofVirtual().start(() -> {
    try {
        String job = queue.take();
        System.out.println("Processing " + job);
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }
});

A bounded queue gives natural backpressure, because a fast producer is forced to wait for slow consumers. Choose LinkedBlockingQueue for optional bounds and PriorityBlockingQueue when jobs have priorities. Restore the interrupt flag, as shown, when you catch InterruptedException.

Take quiz
What does BlockingQueue.put() do when the queue is full?
It silently drops the element
It throws NullPointerException
It blocks until space becomes available
Why prefer a bounded queue in Producer-Consumer?
It makes elements immutable
It guarantees FIFO across JVMs
It applies backpressure to fast producers

48. How does Dependency Injection work without a framework?

Dependency Injection simply means a class receives its collaborators from outside instead of creating them. You can do that with plain constructors and one place that wires everything together, often called the composition root.

public class OrderService {
    private final PaymentGateway gateway;
    private final Clock clock;

    public OrderService(PaymentGateway gateway, Clock clock) {
        this.gateway = gateway;
        this.clock = clock;
    }
}

public static void main(String[] args) {
    var service = new OrderService(new StripeGateway(), Clock.systemUTC());
}

In a test you pass a fake gateway and Clock.fixed(...), so no framework or mocking library is needed. Prefer constructor injection because the object is never half-configured and fields can be final. Frameworks like Spring just automate this wiring.

Take quiz
What is the composition root?
The single place where the object graph is created and wired
A base class all services must extend
A static singleton holding every object
Why is constructor injection preferred over field injection?
Dependencies can be final and the object is never half-configured
It is the only way to use interfaces
It avoids needing any constructors

49. How do you break a Singleton and how do you prevent it?

A plain Singleton with a private constructor can be broken in four ways, and each has a known defense.

Attack Why it works Defense
Reflection setAccessible(true) calls the private constructor Throw if an instance exists, or use an enum
Serialization Deserializing builds a new object Add readResolve() returning the instance, or use an enum
Cloning clone() copies the object Do not implement Cloneable, or throw in clone()
Multiple class loaders Each loader has its own copy of the class Load it from one shared parent loader

private Object readResolve() {
    return INSTANCE;
}

An enum Singleton avoids the first three problems out of the box. Strong encapsulation since Java 17 blocks reflection into JDK internals, but it does not protect your own classes unless they are in a module that does not open its packages.

Take quiz
Which method prevents a deserialized Singleton from becoming a second instance?
finalize()
writeReplace() on the caller
readResolve()
hashCode()
Which Singleton style avoids reflection, serialization and clone attacks by default?
Eager static field
Enum Singleton
Double-checked locking
Holder idiom

50. How do you design an immutable class in Java 21?

An immutable object cannot change after construction, which makes it automatically thread-safe and safe to share. The simplest route in Java 21 is a record, and for a hand-written class you follow these rules.

  1. Declare the class final so nobody can subclass it and add mutability.
  2. Make every field private final and provide no setters.
  3. Copy mutable inputs in the constructor, for example List.copyOf().
  4. Never return a mutable internal object. Return an unmodifiable copy or view.
  5. Do not let this escape from the constructor.
public final class Schedule {
    private final List<LocalDate> dates;

    public Schedule(List<LocalDate> dates) {
        this.dates = List.copyOf(dates);
    }

    public List<LocalDate> dates() {
        return dates;   // already unmodifiable
    }
}

Arrays are the usual trap. They are always mutable, so clone them on the way in and out.

Take quiz
Why is an immutable object automatically thread-safe?
The JVM locks every immutable object
It is stored only on the stack
Its state cannot change after construction, so there is nothing to race on
What does List.copyOf() do in an immutable class constructor?
Sorts the list before storing it
Wraps the list with a synchronized view
Creates an unmodifiable copy that the caller cannot change afterwards
«
»

Comments & Discussions