Java / Core Java 21 Design Patterns Interview questions
Last updated
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 ready-made library added as a dependency
A reusable solution template for a recurring design problem
A compiler optimization applied to bytecode
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
Creational
Behavioral
Structural
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
Open for extension but closed for modification
Open for modification but closed for extension
Open to all callers and closed to subclasses
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
ArrayList
StringBuilder
LocalDate
Runtime
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
new ArrayList<>()
System.out.println()
List.of(1, 2, 3)
Math.max(1, 2)
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
Families of related objects that belong together
One object with many optional fields
Copies of an existing object
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
Sharing one instance across threads
Telescoping constructors with many optional parameters
Converting between incompatible interfaces
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
It was removed in Java 21
It only works on records
It makes shallow copies by default and bypasses constructors
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
Byte streams and character streams
Threads and processes
Strings and arrays
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
File
BufferedReader
Path
Console
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
A way to clone objects cheaply
A single global instance
A simplified entry point to a complex subsystem
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
Virtual proxy
Protection proxy
Remote proxy
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
It sorts observers by priority
Observers can be added or removed during notification safely
It makes observers immutable
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
Singleton
Strategy
Prototype
Facade
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
Undo, redo and queuing of operations
Automatic memory management
Compile-time exhaustive checks
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
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
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
Comparable
Cloneable
Iterable
Serializable
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
Scattered conditionals that check a status field
Constructor overloading
Memory leaks from listeners
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
Share one instance globally
Treat a leaf and a group of objects uniformly
Wrap an object to add logging
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
0 to 255
-1024 to 1023
0 to 100
-128 to 127
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
The sender is decoupled from the handler that processes the request
It guarantees only one handler class exists
It removes the need for interfaces
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
Reflection only
Double dispatch
Operator overloading
Static imports
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
Slow object creation
Duplicate objects in memory
Tight many-to-many dependencies between components
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
Undo and state checkpoints
Lazy loading of objects
Thread pooling
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
Race conditions in static fields
Class explosion from combining two independent dimensions of variation
Shallow copying of arrays
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
Constructor calls
Interface implementations
Repeated null checks
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
Accessors, equals, hashCode and toString
Setters and a no-arg constructor
A Builder and a clone method
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
abstract, static or volatile
final, sealed or non-sealed
public, private or protected only
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.
- Thread safety: enum constants are created during class initialization, which runs once.
- Serialization safety: deserializing an enum returns the existing constant by name, never a new instance.
- Reflection safety:
Constructor.newInstance()throwsIllegalArgumentExceptionfor 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
Enum constructors are always public
Reflection is disabled inside enums
Constructor.newInstance() rejects enum types with an exception
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
To prevent another thread from seeing a partially constructed object
To make the constructor run faster
To allow serialization of the singleton
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
The final keyword on methods
A sealed hierarchy
Static nested classes
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
Java 14
Java 17
Java 23
Java 21
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
Factory Method
Prototype
Abstract Factory
Singleton
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
Decorator
Proxy
Composite
Bridge
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
The client choosing an algorithm from outside
The garbage collector
The states or the context, based on internal transitions
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
Facade
Mediator
Observer
Adapter
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 sealed interface
A functional interface with one abstract method
An interface with five default methods
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()); }
FileInputStreamreads raw bytes from a file.InputStreamReaderdecodes bytes into characters.BufferedReaderadds buffering andreadLine().
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
Character decoding from bytes
File system access
Buffering and the readLine() method
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
In the record's compact constructor
Only inside the Builder setters
In a static initializer block
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
The parent class cannot be instantiated
Subclasses break when the parent's internal implementation changes
Subclasses cannot override public methods
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.
Observableis 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
Objectvalues, 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
It is a final interface
It cannot hold more than one observer
It is a class, so it consumes your single superclass slot
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
By calling Subscription.request(n)
By sleeping inside onNext only
By closing the 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
uses
provides
exports
requires static
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
Any final class
Only enums
Only records
Interfaces only
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
Create one virtual thread per task without pooling
Reuse a fixed pool of 8 virtual threads
Share one virtual thread among all tasks
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
Small DTO objects
Database connections
Short-lived strings
Virtual threads
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
It silently drops the element
It throws NullPointerException
It blocks until space becomes available
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
The single place where the object graph is created and wired
A base class all services must extend
A static singleton holding every object
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
finalize()
writeReplace() on the caller
readResolve()
hashCode()
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.
- Declare the class
finalso nobody can subclass it and add mutability. - Make every field
private finaland provide no setters. - Copy mutable inputs in the constructor, for example
List.copyOf(). - Never return a mutable internal object. Return an unmodifiable copy or view.
- Do not let
thisescape 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.