Skip to main content

Top 30 Design Patterns Interview Questions in Java (with Code)

Thirty design patterns interview questions with short answers, runnable Java 25 code and tests that prove each claim: Singleton variants, Factory vs Abstract Factory, Strategy vs State, Decorator vs Proxy, and where Spring uses each pattern. Hub for the 23-pattern GoF series.

An interviewer rarely asks you to recite the Gang of Four. They ask “what is the difference between Decorator and Proxy?” or “why does double-checked locking need volatile?” and watch whether you reason from intent or from memory. These design patterns interview questions are the thirty I would want a candidate to answer, each with a short answer you can say out loud, runnable Java, and a test that proves the claim rather than asserting it.

This page is the hub for the 24-post GoF design patterns series on this blog. Every pattern below links to its full post, and every claim traces to the interview module of the companion repository, asmhatre/design-patterns/interview, where 31 tests produce the console output quoted here.

Versions. JDK 25 (Temurin 25.0.5), Spring Framework 7.0.10 (the newest 7.0.x GA in Maven Central’s maven-metadata.xml on 2026-10-28; spring-context and spring-tx only, no Spring Boot), JUnit 6.1.3, AssertJ 3.27.7. Everything was compiled and run on these; the output is committed under output/.

How to answer design patterns interview questions

A pattern is a named solution to a recurring design problem, and the name is the least important part. Good answers follow the same four beats, and each answer below is built that way:

  1. Intent. What problem does it solve, in one sentence? Two patterns can have identical class diagrams and opposite intents (Strategy and State, Decorator and Proxy), so this is the beat that separates them.
  2. Shape. Which classes, who calls whom, who creates what. Draw it if there is a whiteboard.
  3. Trade-off. What does it cost? Every pattern adds indirection; naming the cost is what makes the answer sound like experience.
  4. A real example. Something in the JDK or Spring that the interviewer has used. Questions 24 to 30 exist for this beat.

If you only have thirty seconds, use the table below. It holds the six pairs interviewers confuse most, each reduced to the one question that decides it.

PairThe deciding questionAnswer
Strategy vs StateWho changes the implementation: the client (Strategy) or the object itself (State)?Q12
Decorator vs ProxyAdds behaviour around something the client supplies, or controls access to something it hides?Q19
Adapter vs FacadeChanges one interface to fit another, or invents a simpler interface over many classes?Q21
Factory Method vs Abstract FactoryOne product via a subclass, or a whole family via one object?Q9
Template Method vs StrategyVary a step by inheritance (compile time) or by composition (runtime)?Q15
Singleton vs Spring singletonOne per class loader enforced by the class, or one per bean definition per container?Q7

The thirty questions are in five groups. Each group is an accordion so the page stays scannable; open the one you need, or use the table to jump.

GroupQuestionsWhat it covers
Singleton: seven questionsQ1–Q7Singleton is the most-asked pattern and the one with the most traps: threads, memory model, serialization, reflection, and what “singleton” means in Spring.
Creational patterns: Factory, Abstract Factory, Builder, PrototypeQ8–Q11Interviewers love the “what is the difference between X and Y” format here. The answers below always come down to how many products and who decides.
Behavioural patterns: Strategy vs State, Observer, Template Method and moreQ12–Q18Most behavioural questions are really “same shape, different intent” questions.
Structural patterns: Decorator vs Proxy, Adapter vs Facade, Composite, FlyweightQ19–Q23Structural patterns are all wrappers. The only way to tell them apart is by intent, so each answer names the intent.
Design patterns in Spring: where the framework uses each oneQ24–Q30The question that separates a candidate who read a book from one who has debugged a Spring application: “which patterns does Spring use, and what breaks because of them?”

Singleton: seven questions (Q1–Q7)

Singleton is the most-asked pattern and the one with the most traps: threads, memory model, serialization, reflection, and what “singleton” means in Spring.

Q1–Q7: Singleton: seven questions

Q1. What is the Singleton pattern, and what does eager initialisation give you?

Short answer. One class, one instance, and a global access point to it. The simplest correct version builds the instance in a static final field, because the JVM runs class initialisation exactly once and under a lock, so you get thread safety without writing any.

public final class EagerSingleton {
    private static final EagerSingleton INSTANCE = new EagerSingleton();

    private EagerSingleton() { }

    public static EagerSingleton getInstance() { return INSTANCE; }
}

Source: EagerSingleton.java. The cost is that the instance exists even if nobody ever calls getInstance(). That only matters if construction is expensive or has side effects.

Q1 eager: getInstance() == getInstance() -> true

Transcript: 01-singleton.txt. Series post: Singleton.

Q2. Why is the textbook lazy Singleton broken in multithreaded code?

Short answer. Because if (instance == null) instance = new ... is a check followed by an act. Two threads can both see null and both construct. The result is two “singletons”.

public static NaiveLazySingleton getInstance() {
    if (instance == null) {          // check-then-act: two threads can both see null
        instance = new NaiveLazySingleton();
    }
    return instance;
}

Source: NaiveLazySingleton.java. The class sleeps 20 ms in its constructor purely to widen the race window so the bug reproduces on demand; without the sleep it still exists, it is just rare. The test releases eight threads together with a CyclicBarrier and counts constructor runs.

Q2 naive lazy, 8 threads: distinct instances=more than 1 constructor runs=8

Transcript: 01-singleton.txt. In this run the constructor executed eight times; the exact count varies, so the test only asserts “more than one”.

Q3. Is making getInstance() synchronized a good answer?

Short answer. It is correct, and it is the answer to give first. Its only weakness is that every call takes the lock, including the millions after initialisation when the lock protects nothing.

public static synchronized SynchronizedSingleton getInstance() {
    if (instance == null) {
        instance = new SynchronizedSingleton();
    }
    return instance;
}

Source: SynchronizedSingleton.java. Same eight-thread race as Q2, now correct:

Q3 synchronized, 8 threads: distinct instances=1 constructor runs=1

I have not benchmarked the lock cost here, so I will not quote a number. An uncontended lock is cheap on a modern JVM; it becomes a concern under real contention on a hot path.

Q4. Why does double-checked locking need volatile?

Short answer. Without it, the Java Memory Model lets another thread see a non-null instance reference before the constructor’s writes are visible. It then uses a half-built object. volatile forbids that reordering and makes the unsynchronised first read safe.

private static volatile DclSingleton instance;
// ...
public static DclSingleton getInstance() {
    DclSingleton local = instance;          // one volatile read on the fast path
    if (local == null) {
        synchronized (DclSingleton.class) {
            local = instance;
            if (local == null) {
                instance = local = new DclSingleton();
            }
        }
    }
    return local;
}

Source: DclSingleton.java; the deliberately broken twin is DclWithoutVolatile.java. Reading the field once into local keeps the fast path to a single volatile read.

Without volatile: the three steps of “instance = new X()” may be reorderedThread A: 1. allocate memoryThread A: 3. instance = ref (published early)Thread A: 2. run constructor (not yet done)Thread B: instance != null, skip lockThread B: reads fields = defaults (0, null)The first null check is outside the lock, so B never synchronises with A.volatile adds a happens-before edge: the constructor’s writes become visible before the reference does.Java 5 (JSR-133) made this work; before it, double-checked locking was broken even with volatile.early publicationunsafe read

The diagram shows why the problem is not about locks: thread B never takes the lock, because it passes the first null check on the very reference thread A has just published. Steps 2 and 3 are free to swap, so B can arrive between them.

Q4 DCL with volatile, 8 threads: distinct instances=1 constructor runs=1
Q4 DclSingleton.instance volatile? true
Q4 DclWithoutVolatile.instance volatile? false
What this test does not prove. I could not make the missing-volatile failure happen on this machine. x86 keeps stores in order, so the broken variant almost always works here, and it can still fail on weaker memory models such as ARM. The test therefore asserts the modifier (present in one class, absent in the other) and the correct behaviour of the fixed one. The claim that the unsafe version is allowed to fail comes from the Java Memory Model (JLS chapter 17), not from my run.

Q5. What is the initialisation-on-demand holder idiom?

Short answer. A private static nested class holds the instance in a static final field. The nested class is not initialised until getInstance() first touches it, and class initialisation is thread-safe, so you get laziness with no locks and no volatile.

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

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

Source: HolderSingleton.java. The test reads the constructor counter first (which initialises only the outer class), then calls getInstance() twice.

Q5 holder before first getInstance(): constructor runs=0
Q5 holder after two getInstance() calls: constructor runs=1 same=true

Transcript: 01-singleton.txt. For most hand-written lazy singletons this is the one to reach for, unless you also need to defend against the attacks in Q6.

Q6. Why is the enum singleton considered safest? What do serialization and reflection do to the others?

Short answer. A classic singleton can be duplicated in two ways: deserialising a serialised copy, and calling its private constructor through reflection. An enum constant resists both because the JVM serialises it by name and refuses to construct enums reflectively.

public enum EnumSingleton {
    INSTANCE;

    private int counter;

    public int next() { return ++counter; }
}
private HardenedSingleton() {
    if (INSTANCE != null) {
        throw new IllegalStateException("already initialised");
    }
}
// ...
private Object readResolve() { return INSTANCE; }

Sources: EnumSingleton.java, ClassicSingleton.java (no protection) and HardenedSingleton.java (readResolve plus a constructor guard). One test round-trips each through ObjectOutputStream and tries setAccessible(true) on each constructor.

Q6 serialisation round trip keeps the same instance? classic=false hardened(readResolve)=true enum=true
Q6 reflection newInstance() on classic private constructor returns the singleton? false
Q6 reflection on hardened constructor: InvocationTargetException caused by java.lang.IllegalStateException: already initialised
Q6 reflection on enum constructor: java.lang.IllegalArgumentException: Cannot reflectively create enum objects
Read the third and fourth lines carefully. The unprotected class loses on both counts. The hardened class survives, but only because its constructor throws once INSTANCE is non-null, so the guard only works for eager initialisation (reasoned, not tested here: with a lazy singleton an attacker can call the constructor before anyone calls getInstance()). The enum needs no code at all, and its refusal message comes straight from the JDK.

If you take only one thing away: readResolve is the fix for serialization, and since Java serialization is a security problem in its own right, see Java serialization: why it is dangerous and what replaced it. Series post: Singleton.

Q7. How is a Spring singleton different from the GoF Singleton?

Short answer. The GoF Singleton makes the class guarantee one instance per JVM (more precisely, per class loader). A Spring singleton is a scope: one instance per bean definition per container. The class itself is an ordinary class you can instantiate freely.

try (AnnotationConfigApplicationContext one = new AnnotationConfigApplicationContext();
     AnnotationConfigApplicationContext two = new AnnotationConfigApplicationContext()) {
    one.registerBean(ScopeDemo.Service.class);
    two.registerBean(ScopeDemo.Service.class);
    one.refresh();
    two.refresh();
Q7 Spring singleton scope: same bean twice from one context? true; same bean across two contexts? false

Transcript: 01-singleton.txt. Two contexts, two beans. This is why “Spring beans are singletons” is true but incomplete, and why a Spring singleton still has to be thread-safe: many threads share the one instance. The scope list and the prototype-in-singleton trap are in Spring bean scopes; Q30 below reproduces that trap.

Going deeper

Creational patterns: Factory, Abstract Factory, Builder, Prototype (Q8–Q11)

Interviewers love the “what is the difference between X and Y” format here. The answers below always come down to how many products and who decides.

Q8–Q11: Creational patterns: Factory, Abstract Factory, Builder, Prototype

Q8. What is the difference between a simple factory and the Factory Method pattern?

Short answer. A simple factory is one method with a switch that picks a class. It is useful but it is not a GoF pattern. Factory Method is an inheritance idea: a base class has the algorithm and an abstract creation step, and a subclass decides which product to create.

public static Button button(String theme) {
    return switch (theme) {
        case "light" -> new LightButton();
        case "dark" -> new DarkButton();
        default -> throw new IllegalArgumentException("unknown theme: " + theme);
    };
}
public abstract class Dialog {
    protected abstract Button createButton();      // the factory method

    public final String show() {
        return "dialog with " + createButton().render();
    }

    public static final class LightDialog extends Dialog {
        @Override protected Button createButton() { return new SimpleFactory.LightButton(); }
    }

    public static final class DarkDialog extends Dialog {
        @Override protected Button createButton() { return new SimpleFactory.DarkButton(); }
    }
}

Sources: SimpleFactory.java and Dialog.java. The extension point differs: to support a new theme in the simple factory you edit the switch; with Factory Method you add a subclass and touch nothing existing.

Q8 simple factory: button("dark") -> [ dark button ]
Q8 factory method: new DarkDialog().show() -> dialog with [ dark button ]

Transcript: 02-creational.txt. Series post: Factory Method.

Q9. Factory Method vs Abstract Factory: what is the actual difference?

Short answer. Factory Method creates one product and relies on inheritance. Abstract Factory is an object that creates a family of related products and guarantees they belong together. If the product count is one, you want Factory Method; if mismatched combinations are a bug, you want Abstract Factory.

Button createButton();

Checkbox createCheckbox();
// ...
GuiFactory LIGHT = new GuiFactory() {
    public Button createButton() { return new SimpleFactory.LightButton(); }
    public Checkbox createCheckbox() { return () -> "light"; }
};

Source: GuiFactory.java. The test asks each factory for a button and a checkbox and checks the themes agree, so a dark button can never end up beside a light checkbox.

Q9 LIGHT factory -> button=light checkbox=light
Q9 DARK factory  -> button=dark checkbox=dark

The trade-off interviewers like to hear: adding a new family (a third theme) is easy, but adding a new product type (a slider) forces a change to the interface and every factory. That follows from the shape of the code rather than from a test.

Series posts: Abstract Factory, Factory Method.

Q10. When do you use Builder instead of a factory?

Short answer. A factory answers “which type?”. A Builder answers “how do I assemble this one object from many optional parts?”. Use it when a constructor would have many parameters, several optional, and you want validation in one place and an immutable result.

public HttpCall build() {
    Objects.requireNonNull(url, "url is required");   // validated once, in one place
    if (timeoutSeconds <= 0) throw new IllegalArgumentException("timeout must be positive");
    return new HttpCall(this);
}

Source: HttpCall.java. build() is the single place where invalid state is rejected, and the built object copies the header map so later changes to the builder cannot leak in. The JDK’s HttpRequest.newBuilder(uri) follows the same shape.

Q10 builder defaults: method=GET timeout=30; headers map mutable? false
Q10 build() without url -> NullPointerException: url is required
Q10 JDK HttpRequest.newBuilder(uri).build() default method=GET

Series post: Builder.

Q11. What goes wrong with the Prototype pattern: shallow copy vs deep copy?

Short answer. Prototype creates new objects by copying an existing one. The trap is a shallow copy: the copy shares its mutable fields with the original, so editing the copy silently edits the original.

public Doc shallowCopy() { return new Doc(title, tags); }
// ...
public Doc deepCopy() { return new Doc(title, new ArrayList<>(tags)); }

Source: Doc.java. The test edits both copies and then looks at the original.

Q11 original tags after both copies were edited: [java, added-via-shallow]
Q11 shallow.tags() == original.tags()? true; deep.tags() == original.tags()? false
Q11 ArrayList.clone() shares elements: original.get(0) is now "a!"

The first line shows the original contains the tag added through the shallow copy, and nothing from the deep copy. The last line shows that ArrayList.clone() is itself shallow, which is a favourite follow-up question. Effective Java (Item 13) recommends copy constructors or copy factories over Cloneable for exactly this reason.

Series post: Prototype.

Going deeper

Behavioural patterns: Strategy vs State, Observer, Template Method and more (Q12–Q18)

Most behavioural questions are really “same shape, different intent” questions.

Q12–Q18: Behavioural patterns: Strategy vs State, Observer, Template Method and more

Q12. Strategy vs State: they look identical, so what is different?

Short answer. The class diagrams are the same: a context delegates to an interface with swappable implementations. The difference is who chooses and why. In Strategy the client picks an algorithm and it stays put. In State the object moves itself between states, and the next state is decided by the current one.

Strategy: the CLIENT changes itClientContextStrategy (iface)setStrategy(s)State: the OBJECT changes itselfClientContextState (iface)advance()client picks, nobody else swaps itstate.next() returns the next state

The diagram states the rule: ask “who changes the implementation?”. If the answer is the caller, it is Strategy; if the answer is the implementation itself, it is State.

interface State {
    State next();
    String name();
}
// ...
private static final State PAID = new State() {
    public State next() { return SHIPPED; }
    public String name() { return "PAID"; }
};
// ...
public void advance() { state = state.next(); }
public void use(DiscountStrategy s) { this.strategy = s; }

Sources: Order.java and Checkout.java. Order has no setter for its state, only advance(); Checkout has only the setter.

Q12 state: advance() x3 -> NEW -> PAID -> SHIPPED -> DELIVERED
Q12 state: advance() on DELIVERED -> IllegalStateException: already delivered
Q12 strategy: total(1000) with NONE=1000.0 then FESTIVAL=900.0

Transcript: 03-behavioral.txt. Series posts: Strategy, State.

Q13. How do lambdas change the Strategy pattern in modern Java?

Short answer. When the strategy is a single-method interface, you do not need a class per algorithm. Any lambda or method reference is a strategy, and java.util.Comparator is the JDK’s own example.

@FunctionalInterface
public interface DiscountStrategy extends DoubleUnaryOperator { }

public static final DiscountStrategy NONE = amount -> amount;
public static final DiscountStrategy FESTIVAL = amount -> amount * 0.90;

Source: Checkout.java. The test sorts with two Comparators and then passes a one-off lambda to use(...).

Q13 sort by length (Comparator strategy): [fig, pear, banana]
Q13 sort reversed alphabetical: [pear, fig, banana]
Q13 inline lambda strategy: total(200)=150.0

Keep a named class when the strategy has state, needs a name in logs, or is injected by a framework (see Q29).

Q14. Observer: what goes wrong when a listener unsubscribes during notification?

Short answer. The subject is iterating its listener list while the listener edits it. With an ArrayList you get either a ConcurrentModificationException or, worse, a silently skipped listener. Iterate a snapshot.

public void publishNaive(String event) {
    for (Consumer<String> l : listeners) l.accept(event);
}
// ...
public void publishSafe(String event) {
    for (Consumer<String> l : List.copyOf(listeners)) l.accept(event);
}

Source: EventSource.java. The test unsubscribes the first listener from inside its own callback.

Q14 publishNaive, 2 listeners, first unsubscribes itself: delivered to [first] (no exception, second was skipped)
Q14 publishNaive, 3 listeners, first unsubscribes itself -> ConcurrentModificationException
Q14 publishSafe, two events, self-unsubscribing listener: [once, other, other]
Q14 java.util.Observable @Deprecated(since="9")
The silent version is the dangerous one. With two listeners, removing the first leaves the iterator’s cursor equal to the new size, so hasNext() returns false and the second listener never hears the event, with no exception. With three, the cursor no longer equals the size and the exception appears. A bug that depends on the listener count is the kind interviewers hope you have met. Also note java.util.Observable is deprecated since Java 9 (the last line), so do not build on it.

Series post: Observer.

Q15. Template Method vs Strategy: both vary a step of an algorithm. When do you pick which?

Short answer. Template Method varies the step by inheritance: the skeleton is fixed in a base class and subclasses fill in blanks, decided at compile time. Strategy varies it by composition: the step is passed in, and can change at runtime. Prefer Strategy unless a framework forces subclassing.

public abstract class ReportJob {
    protected final List<String> log = new ArrayList<>();

    protected abstract String body();                 // the step subclasses fill in

    public final List<String> run() {                  // the template: final, so the order cannot change
        log.add("open");
        log.add(body());
        log.add("close");
        return log;
    }

    public static final class SalesReport extends ReportJob {
        @Override protected String body() { return "sales rows"; }
    }

    /** Same skeleton, but the varying step is injected: no subclass needed, can change at runtime. */
    public static List<String> runWith(Supplier<String> body) {
        return List.of("open", body.get(), "close");
    }
}

Source: ReportJob.java. The skeleton method run() is final so subclasses cannot reorder the steps; the test asserts the modifier through reflection.

Q15 template method run(): [open, sales rows, close]
Q15 strategy version runWith(): [open, ad-hoc rows, close]
Q15 run() is final (subclasses cannot reorder the steps): true

Series posts: Template Method, Strategy.

Q16. What is Chain of Responsibility, and where do you meet it?

Short answer. A request passes along a list of handlers; each handles it, rejects it, or passes it on. The point is that the sender does not know which handler will act, and handlers can be added or reordered without changing the sender.

public String process(String request) {
    for (Handler h : handlers) {
        String response = h.handle(request);
        if (response != null) return response;
    }
    return "no handler";
}

Source: Pipeline.java. The test records which handlers ran, to show both the full path and the early exit.

Q16 valid request   -> 200 ok via [auth, rate-limit, controller]
Q16 no token        -> 401 via [auth]

Servlet filters and Spring Security’s filter chain are the usual real-world answers; I have not run either in this module, so treat that as background rather than a tested claim.

Series post: Chain of Responsibility.

Q17. What is the Command pattern, and why does it make undo easy?

Short answer. Command turns a request into an object. Because the action is a value, you can queue it, log it, and keep it on a stack. Give it an undo() that reverses execute() and undo becomes popping that stack.

public interface Command {
    void execute();
    void undo();
}
// ...
public void run(Command c) {
    c.execute();
    history.push(c);
}
// ...
public void undo() { text.setLength(text.length() - s.length()); }

Source: Editor.java.

Q17 after two commands: "Hello, world"; undo -> "Hello"; undo -> ""

Series posts: Command, Memento.

Q18. Visitor vs a sealed interface with a pattern-matching switch: is Visitor obsolete?

Short answer. For a closed set of types, largely yes. A sealed interface plus a switch over its records does what Visitor does with no accept() methods and no visitor interface, and the compiler checks you covered every case. Visitor still has a place on pre-21 codebases or open hierarchies.

static int eval(Expr e) {
    return switch (e) {
        case Num n -> n.value();
        case Add a -> eval(a.left()) + eval(a.right());
        case Mul m -> eval(m.left()) * eval(m.right());
    };
}

Source: Expr.java. The test evaluates the same expression with the switch and with a classic visitor.

Q18 2 + 3 * 4 via sealed switch=14 via Visitor=14

The exhaustiveness check is the real benefit. To show it, I kept a file outside the build that adds a third subtype and omits its case, and let the compiler speak:

static int eval(Expr e) {
    return switch (e) {
        case Num n -> n.value();
        case Add a -> eval(a.left()) + eval(a.right());
        // no case for Neg and no default
    };
}
Q18Broken.java:9: error: the switch expression does not cover all possible input values
        return switch (e) {
               ^
1 error

Source: Q18Broken.java; compiler output: 06-q18-compile-error.txt, regenerated by scripts/run-all.sh. Pattern matching for switch is final since Java 21; the language side is covered in pattern matching in Java: switch, record patterns and primitive patterns. Series post: Visitor.

Going deeper

  • State and Strategy: both full examples side by side.
  • Iterator and Mediator: the two behavioural patterns not asked above, often paired with Observer in follow-ups.
  • Interpreter: pairs naturally with the sealed Expr type in Q18.

Structural patterns: Decorator vs Proxy, Adapter vs Facade, Composite, Flyweight (Q19–Q23)

Structural patterns are all wrappers. The only way to tell them apart is by intent, so each answer names the intent.

Q19–Q23: Structural patterns: Decorator vs Proxy, Adapter vs Facade, Composite, Flyweight

Q19. Decorator vs Proxy: same interface, so what is the difference?

Short answer. Intent. A Decorator adds behaviour to an object the client gives it, and decorators stack in any order. A Proxy controls access to an object (lazy creation, permission checks, remote calls) and usually decides for itself when the real object exists. It is a heuristic, not a law: the shapes are identical.

Decorator: client composesClientLoggingCachingReal (given)Proxy: proxy controls accessClientProxyReal?Decorators nest around whatever the client hands them.The dashed box is created by the proxy, if and when it decides to.

The diagram makes the contrast concrete: on the left the client builds the stack and chooses what wraps what; on the right the client sees one object and has no idea whether a real one exists yet.

final class LazyProxy implements Storage {
    private RealStorage real;

    public String read(String key) {
        if (real == null) real = new RealStorage();
        return real.read(key);
    }
}
// ...
final class Caching implements Storage {
    private final Storage inner;
    private final Map<String, String> cache = new HashMap<>();
    public Caching(Storage inner) { this.inner = inner; }

    public String read(String key) { return cache.computeIfAbsent(key, inner::read); }
}

Source: Storage.java. LazyProxy constructs RealStorage itself on first use; Caching and Logging take any Storage in their constructor.

Q19 LazyProxy: RealStorage instances before first read=0, after two reads=1
Q19 ProtectionProxy: read("secret-1") -> SecurityException: denied: secret-1
Q19 Decorators stacked Logging(Caching(Real)): read("k") twice -> log=[read k, read k]

Transcript: 04-structural.txt. Series posts: Decorator, Proxy.

Q20. What does the JDK’s dynamic proxy need, and what does it give you?

Short answer. java.lang.reflect.Proxy creates a class at runtime that implements the interfaces you name and forwards every call to one InvocationHandler. It needs interfaces: it cannot proxy a class. The proxy is not an instance of your implementation class.

Greeter proxy = (Greeter) Proxy.newProxyInstance(Greeter.class.getClassLoader(), new Class<?>[] {Greeter.class},
        (p, method, args) -> { calls.add(method.getName()); return method.invoke(target, args); });

Source: StructuralTest.java with the interface in Greeter.java.

Q20 proxy.greet("Ankur") -> hello Ankur; intercepted=[greet]
Q20 Proxy.isProxyClass(proxy.getClass())=true; proxy instanceof Greeter.Impl=false
Q20 newProxyInstance with a class instead of an interface -> IllegalArgumentException (... is not an interface)

This is the mechanism behind Spring’s JDK-proxy AOP, which Q25 contrasts with CGLIB.

Q21. Adapter vs Facade: both wrap something. How do you tell them apart?

Short answer. An Adapter makes an existing class fit an interface the client already expects; it changes the shape. A Facade invents a new, simpler interface over a whole subsystem; it reduces the number of things the client must know.

public static final class FahrenheitAdapter implements CelsiusSensor {
    private final LegacyFahrenheitSensor legacy;
    public FahrenheitAdapter(LegacyFahrenheitSensor legacy) { this.legacy = legacy; }
    public double celsius() { return (legacy.readFahrenheit() - 32) * 5 / 9; }
}
// ...
public static final class HomeTheater {
    private final Lights lights = new Lights();
    private final Projector projector = new Projector();
    private final Sound sound = new Sound();

    public java.util.List<String> watchMovie() {
        return java.util.List.of(lights.dim(), projector.on(), sound.surround());
    }
}

Source: Adapters.java. The adapter converts Fahrenheit to Celsius behind a CelsiusSensor; the facade turns three subsystem calls into one.

Q21 adapter: 98.6F read through CelsiusSensor -> 37.0 C
Q21 facade: watchMovie() -> [lights dimmed, projector on, surround sound on]
Q21 JDK adapter InputStreamReader(InputStream) first char: h

Bridge is the third one people mix up with Adapter: Adapter is applied after the fact to make incompatible things work, Bridge is designed up front so two hierarchies can vary independently. I have not coded Bridge in this module; the series post has a full example.

Series posts: Adapter, Facade, Bridge.

Q22. What is the Composite pattern?

Short answer. Leaves and containers implement one interface, so code that works on a single item also works on a whole tree of them. The classic example is a file system, where size() on a directory sums its children without the caller checking what each child is.

public sealed interface FileNode permits FileNode.File, FileNode.Dir {
    long size();

    record File(String name, long size) implements FileNode { }

    record Dir(String name, List<FileNode> children) implements FileNode {
        public long size() { return children.stream().mapToLong(FileNode::size).sum(); }
    }
}

Source: FileNode.java.

Q22 size() of a leaf=10, of sub dir=25, of root=35

Series post: Composite.

Q23. What is the Flyweight pattern, and where does the JDK use it?

Short answer. Flyweight shares objects that hold only immutable, intrinsic state, and keeps anything that varies (extrinsic state) outside, passed in per call. The JDK’s Integer cache for small values is the textbook instance.

public record Glyph(char symbol) {          // intrinsic, shared
    public String draw(int x, int y) { return symbol + "@" + x + "," + y; }   // x,y are extrinsic
}
// ...
public Glyph get(char c) { return pool.computeIfAbsent(c, Glyph::new); }

Source: GlyphFactory.java. Eleven characters of “hello world” need only eight distinct Glyph objects, because the letters are shared and the position is passed to draw(x, y).

Q23 "hello world" has 11 characters but the pool holds 8 Glyph objects
Q23 Integer.valueOf: 127==127 -> true, 128==128 -> false

The second line is the classic trap: == on boxed integers works for 127 and fails for 128, because only values in a small range (by default -128 to 127) are cached. Always compare boxed values with equals.

Series post: Flyweight.

Going deeper

Design patterns in Spring: where the framework uses each one (Q24–Q30)

The question that separates a candidate who read a book from one who has debugged a Spring application: “which patterns does Spring use, and what breaks because of them?”

Q24–Q30: Design patterns in Spring: where the framework uses each one

Q24. Where does Spring use the Factory pattern?

Short answer. Everywhere, starting with the container itself: BeanFactory (and so every ApplicationContext) is a factory that hands out beans by name. For objects that need custom construction logic, you implement FactoryBean, and Spring returns the product, not the factory.

public static class ConnectionFactoryBean implements FactoryBean<Connection> {
    public Connection getObject() { return new Connection("jdbc:demo"); }
    public Class<?> getObjectType() { return Connection.class; }
}

Source: Beans.java, test in SpringPatternsTest.java. Asking for the name returns the product; prefixing the name with & returns the factory itself.

Q24 getBean("connection") -> Connection
Q24 getBean("&connection") -> ConnectionFactoryBean
Q24 ApplicationContext is itself a BeanFactory? true

Transcript: 05-spring.txt. For the other Spring-flavoured factory, see static factory methods in Spring beans. Series post: Factory Method.

Q25. How does Spring implement the Proxy pattern: JDK proxy vs CGLIB?

Short answer. Spring AOP wraps your bean in a proxy so it can run advice around calls. If the target implements an interface, the default is a JDK dynamic proxy (Q20); with no interface, or when you force it, Spring generates a CGLIB subclass. The practical difference: you can inject a JDK proxy only as its interface, while a CGLIB proxy is also an instance of your class.

ProxyFactory withInterface = new ProxyFactory(new Shared.GreeterImpl());
Object jdk = withInterface.getProxy();
ProxyFactory forced = new ProxyFactory(new Shared.GreeterImpl());
forced.setProxyTargetClass(true);
Object cglib = forced.getProxy();
Object noInterface = new ProxyFactory(new Shared.Calculator()).getProxy();

Source: SpringPatternsTest.java, using Spring’s own ProxyFactory and AopUtils to classify what comes back.

Q25 target with interface, defaults        -> jdkDynamicProxy=true cglib=false
Q25 target with interface, proxyTargetClass -> jdkDynamicProxy=false cglib=true
Q25 target with no interface                -> jdkDynamicProxy=false cglib=true
Q25 JDK proxy instanceof GreeterImpl? false; CGLIB proxy instanceof GreeterImpl? true

These are ProxyFactory defaults. Frameworks layered on top of it, such as Spring Boot’s AOP auto-configuration, can change which type you get by default, so check the setting in your setup rather than assuming. The AOP side, including why advice sometimes never fires, is in Spring AOP pointcuts and advice: why your aspect is not firing. Series post: Proxy.

Q26. What are the two classic Spring proxy traps: self-invocation and final methods?

Short answer. A proxy only sees calls that go through it. A method calling another method on this skips the proxy, so the advice (transaction, cache, security) does not run. And a CGLIB proxy cannot override final methods, so those run on the proxy object itself, not the real target.

CallerProxyruns advice forouter() / inner()Targetouter()inner()this.inner(): never leaves the target1 advised call (outer). The second call goes straight through “this”, so the proxy never sees it.

The picture shows one advised call where you might expect two. The second arrow never leaves the target, because this is the target, not the proxy.

public static class Calculator {
    public int outer() { return inner() + 1; }       // self-invocation: this.inner(), not proxy.inner()
    public int inner() { return 1; }
    public final String finalMethod() { return "name=" + name; }
    private String name = "real";
}

Source: Shared.java with the tests in SpringPatternsTest.java.

Q26 proxy.outer() returned 2; advice saw [outer] (inner() ran, but was never intercepted)
Q26 proxy.inner() called from outside; advice saw [inner]
Q26 final method on a CGLIB proxy: finalMethod() -> "name=null", advice saw []
The name=null line is the surprising one. The target’s field says "real", yet the call returned null. The proxy is a separate object whose own fields were never initialised (Spring creates CGLIB proxies without running the superclass constructor), and a final method cannot be intercepted, so it executed on the proxy. If a service mysteriously sees null fields, look for a final method on a proxied class.

The same self-invocation trap breaks @Cacheable, @Transactional and @Async; see the Spring cache post for the cache case measured end to end.

Q27. Where does Spring use Observer?

Short answer. In the application event mechanism. A bean publishes an event through ApplicationEventPublisher (the context is one), and any bean with an @EventListener method for that type receives it. Publisher and listener do not know each other.

public static class Listener {
    public final List<String> received = new ArrayList<>();

    @EventListener
    public void on(OrderPlaced e) { received.add(Thread.currentThread().getName()); }
}

Source: Beans.java, test in SpringPatternsTest.java.

Q27 publishEvent(OrderPlaced): listener ran on the caller's thread? true

The listener runs on the publisher’s own thread, so a slow or failing listener slows or fails the publisher. Asynchronous delivery is opt-in (for example with @Async on the listener), and I have not tested that here. Series post: Observer.

Q28. Where does Spring use Template Method?

Short answer. Spring’s *Template classes fix the boring skeleton (open a transaction, run your code, commit or roll back, clean up) and let you plug in the interesting step. Strictly, they use a callback rather than subclassing, so they sit between Template Method and Strategy, which is itself a good thing to say in an interview.

TransactionTemplate template = new TransactionTemplate(tm);
String ok = template.execute(status -> { calls.add("work"); return "done"; });
T.line("Q28 TransactionTemplate success path: %s returned %s", calls, ok);
assertThat(calls).containsExactly("begin", "work", "commit");
calls.clear();
assertThatThrownBy(() -> template.execute(status -> { calls.add("work"); throw new IllegalStateException("boom"); }))
        .isInstanceOf(IllegalStateException.class);
T.line("Q28 TransactionTemplate when the callback throws: %s", calls);
assertThat(calls).containsExactly("begin", "work", "rollback");

Source: SpringPatternsTest.java. The test uses a recording PlatformTransactionManager so no database is needed.

Q28 TransactionTemplate success path: [begin, work, commit] returned done
Q28 TransactionTemplate when the callback throws: [begin, work, rollback]

The skeleton is identical in both runs; only the callback differs, and the template turns the exception into a rollback. I used TransactionTemplate because it needs no database; JdbcTemplate is the other famous one and is not run in this module. Series post: Template Method.

Q29. How does Spring turn Strategy into dependency injection?

Short answer. Declare the strategy as an interface, make each algorithm a bean, and let Spring inject them. Injecting a Map<String, Payment> hands you every implementation keyed by bean name, so adding a strategy is adding a class: no switch to edit.

public static class PaymentService {
    private final Map<String, Shared.Payment> byName;

    @Autowired
    public PaymentService(Map<String, Shared.Payment> byName) { this.byName = byName; }

    public String pay(String method, int amount) { return byName.get(method).pay(amount); }

    public java.util.Set<String> methods() { return new java.util.TreeSet<>(byName.keySet()); }
}

Source: Beans.java, test in SpringPatternsTest.java.

Q29 Map<String, Payment> injected with keys [card, upi]
Q29 pay("upi", 500) -> upi:500

Compare with the hand-wired version in Q12. Series post: Strategy.

Q30. What is the prototype-in-singleton trap?

Short answer. A prototype-scoped bean is created per request to the container. But a singleton is created once, so the prototype it receives at construction time is injected once and then reused forever, which defeats the point. Ask the container each time with ObjectProvider.

public static class StaleHolder {
    private final Ticket ticket;
    @Autowired public StaleHolder(Ticket ticket) { this.ticket = ticket; }
    public Ticket ticket() { return ticket; }
}
// ...
public static class FreshHolder {
    private final ObjectProvider<Ticket> tickets;
    @Autowired public FreshHolder(ObjectProvider<Ticket> tickets) { this.tickets = tickets; }
    public Ticket ticket() { return tickets.getObject(); }
}

Source: Beans.java, test in SpringPatternsTest.java.

Q30 getBean(Ticket) twice from the context: same instance? false
Q30 prototype injected into a singleton: same Ticket on every call? true
Q30 prototype fetched via ObjectProvider: same Ticket on every call? false

Asking the context directly gives a new Ticket every time (first line), but the singleton that had one injected keeps the same one (second), while ObjectProvider restores the prototype semantics (third). Scoped proxies and @Lookup are other fixes that I did not run here. Full coverage: Spring bean scopes and the prototype-in-singleton trap. Series posts: Prototype, Singleton.

Going deeper

How every answer was verified

Each question has a test in one of four classes: SingletonInterviewTest, CreationalAndBehavioralTest, StructuralTest and SpringPatternsTest, all under src/test/java/interview. The tests assert the claim and also write the facts they checked to a transcript, so the console blocks above are quoted verbatim from files that are regenerated by one script: scripts/run-all.sh.

cd interview
./scripts/run-all.sh

The environment and test count it records, from output/00-environment.txt (31 tests for 30 questions, because Q26 has two):

openjdk version "25.0.5" 2026-10-20 LTS
tests: 31

Honest limits: Q4’s failure mode is argued from the Java Memory Model rather than reproduced (explained in that answer); Q2’s exact instance count varies per run; Servlet filters in Q16, JdbcTemplate in Q28, and asynchronous event delivery in Q27 are mentioned as background and were not run. The rest of this repository is plain javac; this module is the one Maven project, because the tests need JUnit and Spring Framework.

Should you memorise all thirty? No. Memorise the six pairs in the table and the four beats, and learn two or three of the code examples well enough to write them without looking. Interviewers are checking whether you can say why, and an answer such as “Spring proxies skip self-invocation, so my @Transactional did nothing” beats any definition. Also be ready to say when a pattern is the wrong tool: a Singleton you could have injected, or a Visitor over a sealed type, is a design smell rather than a sign of sophistication.

Further reading

All 23 pattern posts in the series

PatternCategoryPost
SingletonCreationalsingleton-design-pattern-java
Factory MethodCreationalfactory-method-design-pattern-java
Abstract FactoryCreationalabstract-factory-design-pattern-java
BuilderCreationalbuilder-design-pattern-java
PrototypeCreationalprototype-design-pattern-java
AdapterStructuraladapter-design-pattern-java
BridgeStructuralbridge-design-pattern-java
CompositeStructuralcomposite-design-pattern-java
DecoratorStructuraldecorator-design-pattern-java
FacadeStructuralfacade-design-pattern-java
FlyweightStructuralflyweight-design-pattern-java
ProxyStructuralproxy-design-pattern-java
Chain of ResponsibilityBehaviouralchain-of-responsibility-design-pattern-java
CommandBehaviouralcommand-design-pattern-java
InterpreterBehaviouralinterpreter-design-pattern-java
IteratorBehaviouraliterator-design-pattern-java
MediatorBehaviouralmediator-design-pattern-java
MementoBehaviouralmemento-design-pattern-java
ObserverBehaviouralobserver-design-pattern-java
StateBehaviouralstate-design-pattern-java
StrategyBehaviouralstrategy-design-pattern-java
Template MethodBehaviouraltemplate-method-design-pattern-java
VisitorBehaviouralvisitor-design-pattern-java

No Comments yet!

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.