Skip to content
Posts

cat ~/posts/design-patterns-interview-guide.mdx

2026.10.12

15 min read

โ€“โ€“โ€“ views

ibsanju

Design Patterns for Interviews: Singleton, Factory, Builder, Strategy and More

Eight design patterns explained with the problem each one solves, short Java code, sketched diagrams, real JDK examples and interactive demos.

Series ยท Part 3 of 8

Interview Essentials

  1. [01]Engineering Fundamentals Refresh: OOP, SOLID, ACID, CAP and More
  2. [02]Mastering Java Interviews: Essential Questions and Answers (Basics)
  3. [03]Design Patterns for Interviews: Singleton, Factory, Builder, Strategy and More (you are here)
  4. [04]Essential JavaScript Interview Questions and Answers (Basics)
  5. [05]Essential Coding Patterns for Technical Interviews
  6. [06]Essential Coding Patterns for Technical Interviews (Java)
  7. [07]Essential SQL Commands for Developer Interviews
  8. [08]Essential Concepts for Software Testing Quality Engineers (2025)

A design pattern is a named solution to a design problem that keeps coming up. Interviewers use patterns as shorthand. When they ask about one, they are checking whether you know the problem it solves, not whether you can redraw a class diagram.

This post covers the eight patterns that come up most, in Java. Each one follows the same steps: the problem, shown without the pattern; the idea; the code; where you have already used it in the JDK; how it differs from a look-alike; and what it costs.

It is part 3 of the Interview Essentials series. The principles behind patterns, especially Open/Closed and Dependency Inversion, are in the Engineering Fundamentals Refresh.

The eight patterns grouped by purpose. Creational: Singleton, Factory, Builder. Structural: Adapter, Decorator, Facade. Behavioural: Strategy, Observer.Creationalhow objects are madeSingletonone shared instanceFactoryhide which class is builtBuilderassemble step by stepStructuralhow objects fit togetherAdaptermake interfaces matchDecoratoradd behaviour by wrappingFacadeone simple entry pointBehaviouralhow objects cooperateStrategyswap an algorithmObservernotify many listenersalso worth knowing:Command, Template Method
Three groups, three questions: how is it made, how does it fit together, how does it behave.

How to answer a pattern question

  1. Name the problem first, in one sentence.
  2. Name the pattern that solves it.
  3. Sketch the classes: usually an interface and two or three implementations.
  4. Give one real example and one trade-off.

Creational patterns

1. Singleton

The problem. Some things should exist once: configuration, a connection pool, a registry. If every caller writes new Config(), you get several copies that can disagree, and each one loads the file again.

The idea. Make the constructor private and hand out one instance through a static method.

Three services all call getInstance and receive the same single Config object. The Config constructor is private, so no one else can create another.Service AService BService CConfig.getInstance()Config- static instance- Config() (private)+ static getInstance()Same object every time. That is the whole guarantee.
Every caller gets the same object. Nobody else can call new Config().

The follow-up that always comes: thread safety. The obvious lazy version is wrong when two threads call it at once. Step through it:

Try it: two threads and a lazy singleton

The most common follow-up question: why is lazy initialisation unsafe? Step through the same two threads with and without synchronization.

Version
public static Config getInstance() {  if (instance == null) {    instance = new Config();  }  return instance;}

instance

null

lock held by

nobody

objects created

0

Step 1 of 7

Threads A and B call getInstance() at about the same time. instance is null.

Three ways to get it right:

// 1. Enum: simplest, and safe against reflection and serialization tricks
public enum Config {
  INSTANCE;
  public String get(String key) { /* ... */ return ""; }
}
 
// 2. Holder idiom: lazy and thread-safe, with no explicit locking
public final class Config {
  private Config() {}
  private static class Holder { static final Config INSTANCE = new Config(); }
  public static Config getInstance() { return Holder.INSTANCE; }
}
 
// 3. Double-checked locking: the field must be volatile
public final class Config {
  private static volatile Config instance;
  private Config() {}
  public static Config getInstance() {
    if (instance == null) {
      synchronized (Config.class) {
        if (instance == null) instance = new Config();
      }
    }
    return instance;
  }
}

The holder idiom works because the JVM initialises a class once, on first use, and does so thread-safely. The Holder class is not loaded until getInstance() is first called.

Where you have seen it. Runtime.getRuntime() returns the single runtime object of the JVM. Spring beans are singletons by default, but that means one instance per container, not a private constructor.

Compare. A static utility class also gives you one shared thing, but it cannot implement an interface and cannot be swapped for a fake in a test. A singleton can.

Trade-offs. It is global state in disguise: any code can reach it, so dependencies become hidden and tests interfere with each other. In most applications it is better to create one instance and pass it in, and let a dependency injection container make it single.

Check yourself

Which Singleton implementation is also safe against reflection and serialization attacks?

2. Factory

The problem. Code that decides which class to build ends up repeated everywhere:

Notification n;
if (type.equals("email")) n = new EmailNotification();
else if (type.equals("sms")) n = new SmsNotification();
else throw new IllegalArgumentException(type);

Adding a push notification means finding and editing every copy of that block.

The idea. Move the decision into one place that returns the interface type.

A client asks NotificationFactory to create a notification by type. The factory returns a Notification. EmailNotification and SmsNotification implement Notification. The client only knows the interface.Clientcreate("sms")NotificationFactory+ create(type)returnsNotification+ send(message)EmailNotif.+ send()SmsNotif.+ send()Adding PushNotification means one new class and one new line in the factory.
The client names a type. The factory decides which class to build.
interface Notification { void send(String message); }
class EmailNotification implements Notification { public void send(String m) { /* ... */ } }
class SmsNotification implements Notification { public void send(String m) { /* ... */ } }
 
class NotificationFactory {
  static Notification create(String type) {
    return switch (type) {
      case "email" -> new EmailNotification();
      case "sms" -> new SmsNotification();
      default -> throw new IllegalArgumentException("Unknown type: " + type);
    };
  }
}
 
Notification n = NotificationFactory.create("sms");
n.send("Your code is 4821");

Where you have seen it. Calendar.getInstance(), NumberFormat.getInstance(), List.of(...) and Integer.valueOf(...) are all static factory methods. You never name the concrete class they return.

Compare. The version above is usually called a simple factory or static factory. The Gang of Four's Factory Method is a little different: a base class declares a create() method and each subclass decides which object to return. In an interview, say which one you mean. A factory decides which object you get. A builder, next, controls how one complex object is put together.

Trade-offs. One place to change when types are added, and callers depend only on the interface. The cost is an extra class, and a factory with a growing switch can itself need refactoring once there are many types.

3. Builder

The problem. A class with many fields, most of them optional, leads to this:

new User("Ana", 30, null, null, true, false);   // what are the last four?

The usual workaround, one constructor per combination, does not scale.

The idea. Collect the values one named step at a time, then create the object once.

A User.Builder collects name, age and email through chained calls. Calling build returns an immutable User.User.Builder+ name(String)+ age(int)+ email(String)+ build()build()User- name (final)- age (final)- email (final)no settersnew User.Builder().name("Ana").age(30).build()
Set only what you need, then build once. The result cannot change afterwards.
public final class User {
  private final String name;
  private final int age;
  private final String email;
 
  private User(Builder b) { name = b.name; age = b.age; email = b.email; }
 
  public static class Builder {
    private String name;
    private int age;
    private String email;
 
    public Builder name(String name) { this.name = name; return this; }
    public Builder age(int age) { this.age = age; return this; }
    public Builder email(String email) { this.email = email; return this; }
 
    public User build() {
      if (name == null) throw new IllegalStateException("name is required");
      return new User(this);
    }
  }
}
 
User u = new User.Builder().name("Ana").age(30).build();

Each setter returns this, which is what makes the chain possible. build() is the single place to validate.

Where you have seen it. StringBuilder.append(...), HttpRequest.newBuilder() (Java 11) and Stream.builder(). Lombok's @Builder generates this code for you, but Lombok is a library, not part of the JDK.

Compare. For a small immutable data class, a Java record (Java 16 and later) gives you a constructor and getters in one line and often removes the need for a builder. Reach for a builder when there are many optional fields.

Trade-offs. Readable construction and an immutable result, but roughly twice the code of a plain constructor.

Structural patterns

4. Adapter

The problem. You need to use a class you cannot change, such as a third-party library or legacy code, but its methods do not match the interface your code expects.

The idea. Write a small class that implements your interface and translates each call into the call the old class understands.

The client talks to the PaymentGateway interface. BankAdapter implements PaymentGateway and translates each call into the different method that the legacy bank API expects.ClientPaymentGateway+ pay(amount)BankAdapter+ pay(amount)implementsLegacyBankApi+ makeTransfer( cents, acct)callspay(12.50) becomes makeTransfer(1250, account).
The adapter translates. Neither side has to change.
interface PaymentGateway { void pay(double amount); }
 
class LegacyBankApi {                       // cannot be changed
  void makeTransfer(long cents, String account) { /* ... */ }
}
 
class BankAdapter implements PaymentGateway {
  private final LegacyBankApi bank;
  private final String account;
 
  BankAdapter(LegacyBankApi bank, String account) { this.bank = bank; this.account = account; }
 
  public void pay(double amount) {
    bank.makeTransfer(Math.round(amount * 100), account);   // translate
  }
}

Where you have seen it. InputStreamReader adapts a byte stream (InputStream) to a character stream (Reader). Arrays.asList(array) presents an array through the List interface.

Trade-offs. Neither side changes, and you can swap the legacy API later by writing another adapter. The cost is one more class and a translation layer to keep correct.

5. Decorator

The problem. You want optional features in any combination. With subclasses, three add-ons already mean eight combinations, each its own class (EspressoWithMilk, EspressoWithMilkAndSugar and so on), and every new add-on doubles the count.

The idea. Wrap the object in another object that has the same interface, adds its own part, and passes the call inward. Wrappers can stack.

Nested wrappers. A Coffee is wrapped by Milk, which is wrapped by Sugar. Calling cost on the outer Sugar calls cost on Milk, which calls cost on Coffee, and each layer adds its own price.SugarMilkEspressocost() = 2.00sugar.cost() = 0.20 + milk.cost() = 0.20 + 0.50 + espresso.cost() = 0.20 + 0.50 + 2.00 = 2.70Each layer adds its part andpasses the call inward.
Each wrapper has the same interface as the thing it wraps, so wrappers can stack.
interface Coffee { double cost(); String description(); }
 
class Espresso implements Coffee {
  public double cost() { return 2.00; }
  public String description() { return "Espresso"; }
}
 
abstract class CoffeeDecorator implements Coffee {
  protected final Coffee inner;
  CoffeeDecorator(Coffee inner) { this.inner = inner; }
}
 
class Milk extends CoffeeDecorator {
  Milk(Coffee inner) { super(inner); }
  public double cost() { return inner.cost() + 0.50; }
  public String description() { return inner.description() + ", milk"; }
}
 
Coffee order = new Sugar(new Milk(new Espresso()));   // Sugar works the same way as Milk

Try it: stack decorators

Each add-on wraps the order so far. No new subclass is needed for any combination.

Coffee order = new Sugar(new Milk(new Espresso()));

description()

Espresso, milk, sugar

cost()

2.70

  1. Sugar.cost() = 0.20 + inner.cost()
  2. Milk.cost() = 0.50 + inner.cost()
  3. Espresso.cost() = 2.00

Where you have seen it. Java I/O is built this way:

new BufferedReader(new InputStreamReader(new FileInputStream("data.txt")));

FileInputStream reads bytes. InputStreamReader adapts bytes to characters. BufferedReader decorates that with buffering and readLine(). Collections.unmodifiableList(list) also wraps a list in the same interface while blocking changes.

Compare. Inheritance fixes the combination at compile time. Decorators are chosen and stacked at run time.

Trade-offs. Flexible combinations without a class explosion. The costs: many small classes, the order of wrapping can matter, and you cannot easily remove a layer from the middle.

6. Facade

The problem. Placing an order means calling inventory, payment and shipping in the right order, and every caller repeats it.

The idea. Put one simple class in front of the complicated set of classes.

A client calls one method, placeOrder, on OrderFacade. The facade coordinates the Inventory, Payment and Shipping subsystems behind it.ClientplaceOrder()OrderFacade+ placeOrder()InventoryPaymentShippingThe client does not know the three subsystems exist.
One simple call in front. The messy coordination stays behind it.
class OrderFacade {
  private final Inventory inventory;
  private final Payment payment;
  private final Shipping shipping;
 
  OrderFacade(Inventory i, Payment p, Shipping s) { inventory = i; payment = p; shipping = s; }
 
  OrderResult placeOrder(Order order) {
    inventory.reserve(order.items());
    payment.charge(order.customer(), order.total());   // real code releases the reservation if this fails
    shipping.schedule(order);
    return OrderResult.confirmed(order.id());
  }
}

Where you have seen it. SLF4J is literally the "Simple Logging Facade for Java". Your code calls LoggerFactory.getLogger(...), and one of several logging frameworks does the work behind it.

Trade-offs. Callers get a small, stable API. Watch for two problems: a facade that grows into a class that does everything, and a facade that hides subsystems some callers still need to reach directly.

Adapter, Decorator and Facade side by side

These three look alike on a class diagram and are the most confused trio.

AdapterDecoratorFacade
GoalMake an incompatible interface fitAdd behaviourSimplify a set of classes
InterfaceConverts one interface to anotherSame as the wrapped objectA new, simpler one
WrapsOne objectOne object, often stackedMany objects

Check yourself

You need to add caching to an existing data source without changing its interface or its class. Which pattern fits?

Behavioural patterns

7. Strategy

The problem. Rules that vary pile up in an if/else, and every new rule means editing code that already works:

double total(double subtotal, String customerType) {
  if (customerType.equals("festive")) return subtotal * 0.80;
  if (customerType.equals("bulk")) return subtotal * 0.90;
  return subtotal;
}

The idea. Put each rule behind one interface and let the caller hand in the one it wants.

Checkout holds a DiscountStrategy. NoDiscount, FestiveDiscount and BulkDiscount all implement DiscountStrategy. Checkout calls apply and does not know which one it has.Checkout- strategy+ total(subtotal)has aDiscountStrategy+ apply(price)NoDiscount+ apply()Festive+ apply()Bulk+ apply()No if/else on discount type inside Checkout.
Checkout never changes. Swap the strategy it holds and the behaviour changes.
interface DiscountStrategy { double apply(double price); }
 
class Checkout {
  private final DiscountStrategy strategy;
  Checkout(DiscountStrategy strategy) { this.strategy = strategy; }
  double total(double subtotal) { return strategy.apply(subtotal); }
}
 
// a one-method strategy needs no class when you use a lambda
Checkout festive = new Checkout(price -> price * 0.80);

Try it: swap the strategy

Checkout calls strategy.apply(subtotal) and nothing else. Each item costs 20.00. Change the rule or the item count and watch the total change while the Checkout code stays the same.

class NoDiscount implements DiscountStrategy {
  double apply(double price) {
    return price;
  }
}

subtotal

60.00

total after strategy

60.00

Where you have seen it. The Comparator you pass to list.sort(...) is a strategy: the sorting algorithm stays fixed and you swap the comparison rule. The rejection policies of ThreadPoolExecutor (AbortPolicy, CallerRunsPolicy and others) are strategies too.

Compare. The State pattern looks the same in code, but the object switches its own behaviour as it moves between states. With Strategy, the caller chooses.

Trade-offs. New rules arrive as new classes, and each can be tested alone. The cost is more classes, and callers must know which strategy to pick. For two simple cases, a plain if is fine.

Check yourself

A method picks a shipping cost with a long if/else on the carrier name, and a new carrier is added every quarter. Which pattern removes the if/else?

8. Observer

The problem. One object changes and several others need to react. If the changing object calls each of them directly, it must know about all of them, and every new listener means editing it.

The idea. The changing object (the subject) keeps a list of subscribers behind a small interface and calls each one when something happens. Subscribers can join and leave at run time.

A Newsletter subject keeps a list of subscribers. When publish is called, it calls update on each subscribed observer: an email reader, a mobile app and a Slack bot.Newsletter (subject)- subscribers: List+ subscribe(o)+ unsubscribe(o)+ publish(msg)Email reader+ update(msg)Mobile app+ update(msg)Slack bot+ update(msg)update(msg)
The subject knows only that each subscriber has an update method.
interface Subscriber { void update(String message); }
 
class Newsletter {
  private final List<Subscriber> subscribers = new ArrayList<>();
 
  void subscribe(Subscriber s)   { subscribers.add(s); }
  void unsubscribe(Subscriber s) { subscribers.remove(s); }
  void publish(String message)   { for (Subscriber s : subscribers) s.update(message); }
}
 
newsletter.subscribe(msg -> System.out.println("Email: " + msg));

Try it: subscribe and publish

The newsletter does not know what its subscribers do. It only calls update on each one that is subscribed.

Subscribed observers (2)
  1. Nothing published yet.

Where you have seen it. addEventListener in the browser, ActionListener in Swing and PropertyChangeListener in java.beans. Java's own java.util.Observable and Observer classes have been deprecated since Java 9, so use a listener interface or the java.util.concurrent.Flow interfaces instead.

Compare. With Observer, many listeners are told something happened. With Strategy, one behaviour is chosen.

Trade-offs. The subject and its listeners are loosely coupled. The costs: update order is not obvious, one update can trigger a chain of others that is hard to trace, and a listener that is never unsubscribed stays in memory.

Check yourself

A long-lived object keeps a list of listeners, and some listeners are never removed. What is the most likely problem?

Also worth knowing

PatternOne lineWhere you have seen it
ProxyA stand-in that controls access to another objectjava.lang.reflect.Proxy; Spring uses proxies to add transactions
CommandWrap a request as an object so it can be queued or undoneA Runnable submitted to an ExecutorService
Template MethodA fixed skeleton with steps that subclasses fill inHttpServlet.service() calls doGet or doPost
IteratorWalk a collection without exposing how it is storedjava.util.Iterator and the enhanced for loop
StateBehaviour changes with the object's internal stateAn order moving through placed, paid, shipped

Which pattern for which problem

If the problem is...Reach for
Exactly one shared instance is neededSingleton, or let a DI container make it single
Callers should not name concrete classesFactory
Many optional constructor argumentsBuilder
An existing class has the wrong interfaceAdapter
Add features in any combination at run timeDecorator
Callers face a tangle of classesFacade
Swap an algorithm or a business ruleStrategy
Many objects must react to one changeObserver

Common mistakes

  • Naming a pattern without saying which problem it solves. Start with the problem.
  • Adding a pattern to code that does not need one. A switch with two stable cases does not need Strategy.
  • Using Singleton for something that should simply be passed in.
  • Mixing up look-alikes: Adapter, Decorator and Facade; Strategy and State.

Sources and further reading

  • Gamma, Helm, Johnson and Vlissides, Design Patterns: Elements of Reusable Object-Oriented Software (1994): the original catalogue.
  • Joshua Bloch, Effective Java, third edition (2018): Item 1 on static factory methods, Item 2 on builders and Item 3 on singletons.
  • Freeman and Robson, Head First Design Patterns, second edition (2020): a gentler, example-led introduction.
  • The SLF4J user manual, for the facade example.
  • The Java SE API documentation for java.io, java.util.Comparator and java.net.http.HttpRequest.

This is part 3 of the Interview Essentials series. Next is the JavaScript guide.

Enjoyed this? Leave a reaction

Share this article

Comments

Loading commentsโ€ฆ

Leave a comment

Enjoying this post?

Don't miss out. Get an email whenever I post, no spam.

Subscribe Now