Interview Q&A · 80 questions · 26 answered publicly
Design Patterns Interview Questions
Ordered by how often they're asked. Sign in free to see every answer, rate your confidence and run timed mock interviews.
-
Most asked What is a design pattern, and why are they useful?
Summary
A design pattern is a named, reusable solution shape for a design problem that keeps recurring. Patterns give teams a shared vocabulary and proven trade-offs, so one word like "Strategy" can explain a whole design.
Longer Answer
The Gang of Four book (1994) describes each pattern by its name, the problem it addresses, the solution (roles and collaborations, not finished code) and its consequences. Patterns are not libraries you install; you arrange your own code in that shape, adapted to your language.
Their main value is communication and judgement: recognising "this is an Adapter situation" brings known benefits and costs with it. They are tools for specific problems, not goals. Simple code without patterns is often the best design.
Key Points
- Named solution shapes for recurring design problems.
- Name, problem, solution, consequences.
- Shared vocabulary in design discussions and reviews.
- Use when the problem exists, not by default.
Deep dive: Design Patterns, Explained Simply: What a Design Pattern Is
-
Most asked Explain the SOLID principles.
Summary
SOLID is five object-oriented design principles: Single Responsibility (one reason to change), Open/Closed (extend without modifying), Liskov Substitution (subtypes keep the parent's promises), Interface Segregation (small, focused interfaces) and Dependency Inversion (depend on abstractions, not concrete details).
Longer Answer
- S: a class should answer to one group of people, so changes for different reasons don't collide.
- O: add behaviour by adding code (a new class or strategy), not by editing code that already works.
- L: any code that works with a parent type must work with a subtype without surprises.
- I: clients shouldn't depend on methods they don't use; split large interfaces.
- D: high-level policy and low-level details both depend on abstractions, which the high-level side owns. Dependencies are passed in.
They are guidelines that make code easier to test and change. Applied mechanically, they can produce too many tiny classes; use them where changes actually collide.
Key Points
- Five principles for maintainable OO design.
- Each one reduces the cost of a specific kind of change.
- DIP is the principle; DI is the technique.
- Guidelines, not laws: apply where change happens.
-
Most asked What is the difference between dependency inversion, dependency injection and inversion of control?
Summary
Dependency inversion is a design principle (high-level code depends on abstractions it owns). Dependency injection is a technique for supplying dependencies from outside (usually through the constructor). Inversion of control is the broader idea that a framework or container calls your code and manages wiring, instead of your code controlling everything.
Longer Answer
- DIP (the D in SOLID):
OrderServicedepends on anOrderStoreinterface, not onMySqlOrderStore. - DI:
OrderService(store)receives its store; a composition root or container creates the real one. - IoC: "don't call us, we'll call you"; frameworks call your controllers, event handlers and lifecycle hooks. DI containers are one form of IoC.
You can apply DIP and DI with plain constructors and no framework at all.
Key Points
- DIP: principle about the direction of dependencies.
- DI: technique for passing dependencies in.
- IoC: framework controls the flow and wiring.
- DI doesn't require a container.
Deep dive: Design Patterns, Explained Simply: Dependency Injection Without a Framework
- DIP (the D in SOLID):
-
Most asked Why is composition often preferred over inheritance?
Summary
Composition (an object holds other objects it delegates to) lets you combine behaviours freely and change them at runtime, without the class explosion and tight coupling of deep inheritance hierarchies.
Longer Answer
Using inheritance to mix features (walking, flying, swimming robots) needs a subclass per combination, couples every subclass to its parent's internals (the fragile base class problem), and fixes behaviour at compile time. With composition, a
Robotholds aMovementobject, so three movement classes cover every robot and a robot can switch movement at runtime.Inheritance is still right for genuine "is-a" relationships with stable shared behaviour, such as a framework's base controller.
Key Points
- Avoids class explosion for combinations.
- Allows runtime changes.
- Looser coupling than base classes.
- Inheritance still fits real is-a relationships.
Deep dive: Design Patterns, Explained Simply: Program to Interfaces, Favour Composition
-
Most asked What is dependency injection, and what forms does it take?
Summary
Dependency injection means an object receives its collaborators from outside instead of creating them. The forms are constructor injection (the default), setter or property injection (optional or changeable dependencies) and method injection (one call needs it).
Longer Answer
DI makes dependencies explicit and swappable: tests pass fakes (an in-memory mailer, a fixed clock), and production wires real implementations in one composition root. Frameworks like Spring, Laravel's container, NestJS and FastAPI's
Dependsautomate the wiring.Inject things that do I/O, hold state or need faking: databases, HTTP clients, clocks, random numbers. Ten constructor parameters signal an SRP problem, not a DI problem.
Code Example
class FixedClock: def today(self): return "2030-01-01" class Signup: def __init__(self, clock): # constructor injection self.clock = clock def welcome(self, email): return f"{email} joined on {self.clock.today()}" print(Signup(FixedClock()).welcome("[email protected]"))interface Clock { today(): string; } class FixedClock implements Clock { today() { return "2030-01-01"; } } class Signup { constructor(private clock: Clock) {} // constructor injection welcome(email: string) { return `${email} joined on ${this.clock.today()}`; } } console.log(new Signup(new FixedClock()).welcome("[email protected]"));public class Main { public static void main(String[] args) { System.out.println(new Signup(new FixedClock()).welcome("[email protected]")); } } interface Clock { String today(); } class FixedClock implements Clock { public String today() { return "2030-01-01"; } } class Signup { private final Clock clock; Signup(Clock clock) { this.clock = clock; } // constructor injection String welcome(String email) { return email + " joined on " + clock.today(); } }<?php interface Clock { public function today(): string; } class FixedClock implements Clock { public function today(): string { return "2030-01-01"; } } class Signup { public function __construct(private Clock $clock) {} // constructor injection public function welcome(string $email): string { return "$email joined on {$this->clock->today()}"; } } echo (new Signup(new FixedClock()))->welcome("[email protected]"), "\n";Key Points
- Pass dependencies in.
- Constructor injection is the default.
- Composition root wires real objects.
- Too many dependencies = too many responsibilities.
Deep dive: Design Patterns, Explained Simply: Dependency Injection Without a Framework
-
Most asked Explain the Factory Method pattern and how it differs from a simple factory.
Summary
Factory Method lets subclasses decide which product to create by overriding a creation method that a shared algorithm calls. A simple factory is one function or class with a conditional that returns the right object; it isn't a GoF pattern but solves the same 'who decides the class' problem without subclassing.
Longer Answer
In Factory Method, the creator's algorithm (
export()) callscreate_formatter();CsvReportandJsonReportoverride it. The algorithm only depends on the product interface, so new products need no changes to it. It suits frameworks where users extend a base class.A simple factory (
create_formatter("csv"), or a dictionary from names to classes) is more common in application code. With first-class functions, passing the product or a function that creates it is often simplest.Code Example
class Csv: def format(self, row): return ",".join(row) class Pipe: def format(self, row): return " | ".join(row) class Report: def create_formatter(self): # the factory method raise NotImplementedError def export(self, row): return self.create_formatter().format(row) class CsvReport(Report): def create_formatter(self): return Csv() class PipeReport(Report): def create_formatter(self): return Pipe() for report in [CsvReport(), PipeReport()]: print(report.export(["1", "Ada"]))interface Formatter { format(row: string[]): string; } class Csv implements Formatter { format(row: string[]) { return row.join(","); } } class Pipe implements Formatter { format(row: string[]) { return row.join(" | "); } } abstract class Report { protected abstract createFormatter(): Formatter; // the factory method export(row: string[]) { return this.createFormatter().format(row); } } class CsvReport extends Report { protected createFormatter() { return new Csv(); } } class PipeReport extends Report { protected createFormatter() { return new Pipe(); } } for (const report of [new CsvReport(), new PipeReport()]) console.log(report.export(["1", "Ada"]));import java.util.List; public class Main { public static void main(String[] args) { for (Report report : List.of(new CsvReport(), new PipeReport())) { System.out.println(report.export(List.of("1", "Ada"))); } } } interface Formatter { String format(List<String> row); } class Csv implements Formatter { public String format(List<String> row) { return String.join(",", row); } } class Pipe implements Formatter { public String format(List<String> row) { return String.join(" | ", row); } } abstract class Report { protected abstract Formatter createFormatter(); // the factory method String export(List<String> row) { return createFormatter().format(row); } } class CsvReport extends Report { protected Formatter createFormatter() { return new Csv(); } } class PipeReport extends Report { protected Formatter createFormatter() { return new Pipe(); } }<?php interface Formatter { public function format(array $row): string; } class Csv implements Formatter { public function format(array $row): string { return implode(",", $row); } } class Pipe implements Formatter { public function format(array $row): string { return implode(" | ", $row); } } abstract class Report { abstract protected function createFormatter(): Formatter; // the factory method public function export(array $row): string { return $this->createFormatter()->format($row); } } class CsvReport extends Report { protected function createFormatter(): Formatter { return new Csv(); } } class PipeReport extends Report { protected function createFormatter(): Formatter { return new Pipe(); } } foreach ([new CsvReport(), new PipeReport()] as $report) echo $report->export(["1", "Ada"]), "\n";Key Points
- Creator calls an overridable creation method.
- Subclasses choose the concrete product.
- Simple factory = one creating function/lookup.
- Java's Collection.iterator() is a factory method.
Deep dive: Design Patterns, Explained Simply: Factory Method
-
Most asked What problem does the Builder pattern solve? Show an example.
Summary
Builder assembles complex objects step by step with named, chainable methods and a final
build()that validates the combination. It replaces telescoping constructors and unreadable positional arguments.Longer Answer
A builder keeps defaults, lets callers set only what they need (
.timeout(5).header(...)), and checks invalid combinations (a GET with a body) inbuild(), close to where the object is assembled. The product can be immutable.In Python, PHP 8 and TypeScript, named arguments and object literals remove much of the need; builders still shine for validation, fluent APIs (query builders) and test data builders.
Code Example
class PizzaBuilder: def __init__(self): self.size, self.toppings = "medium", [] def large(self): self.size = "large" return self # returning self allows chaining def topping(self, name): self.toppings.append(name) return self def build(self): if len(self.toppings) > 3: raise ValueError("max 3 toppings") return f"{self.size} pizza with {', '.join(self.toppings) or 'cheese'}" print(PizzaBuilder().large().topping("olives").topping("basil").build()) print(PizzaBuilder().build())class PizzaBuilder { private size = "medium"; private toppings: string[] = []; large(): this { this.size = "large"; return this; } // returning this allows chaining topping(name: string): this { this.toppings.push(name); return this; } build(): string { if (this.toppings.length > 3) throw new Error("max 3 toppings"); return `${this.size} pizza with ${this.toppings.join(", ") || "cheese"}`; } } console.log(new PizzaBuilder().large().topping("olives").topping("basil").build()); console.log(new PizzaBuilder().build());import java.util.ArrayList; import java.util.List; public class Main { public static void main(String[] args) { System.out.println(new PizzaBuilder().large().topping("olives").topping("basil").build()); System.out.println(new PizzaBuilder().build()); } } class PizzaBuilder { private String size = "medium"; private final List<String> toppings = new ArrayList<>(); PizzaBuilder large() { size = "large"; return this; } // returning this allows chaining PizzaBuilder topping(String name) { toppings.add(name); return this; } String build() { if (toppings.size() > 3) throw new IllegalStateException("max 3 toppings"); return size + " pizza with " + (toppings.isEmpty() ? "cheese" : String.join(", ", toppings)); } }<?php class PizzaBuilder { private string $size = "medium"; private array $toppings = []; public function large(): static { $this->size = "large"; return $this; } // returning $this allows chaining public function topping(string $name): static { $this->toppings[] = $name; return $this; } public function build(): string { if (count($this->toppings) > 3) throw new LogicException("max 3 toppings"); return "{$this->size} pizza with " . (implode(", ", $this->toppings) ?: "cheese"); } } echo (new PizzaBuilder())->large()->topping("olives")->topping("basil")->build(), "\n"; echo (new PizzaBuilder())->build(), "\n";Key Points
- Named, chainable steps; build() validates.
- Replaces telescoping constructors.
- Product can be immutable.
- Named arguments often suffice in Python/PHP/TS.
Deep dive: Design Patterns, Explained Simply: Builder
-
Most asked How do you implement a Singleton, and why is it often considered an anti-pattern?
Summary
A Singleton has a private constructor and a static accessor that creates the single instance on first use and returns it afterwards. It's criticised because its global access hides dependencies, shares mutable state across tests and requests, and makes swapping implementations hard.
Longer Answer
The "exactly one instance" guarantee is sometimes useful (a connection pool, a hardware interface), but the global access point is the problem: any code can reach it, tests leak state into each other, and parallel code needs thread safety (in Java: a synchronized accessor, the holder idiom, or an enum).
The usual alternative: create one instance at startup and inject it, or register it with singleton scope in a DI container (Spring's default scope, Laravel's
singleton()). In Python, a module-level object is the idiomatic singleton.Code Example
class Config: _instance = None @classmethod def instance(cls): if cls._instance is None: # created lazily, on first use cls._instance = cls() return cls._instance print("same object:", Config.instance() is Config.instance())class Config { private static _instance: Config | null = null; private constructor() {} static instance(): Config { if (Config._instance === null) Config._instance = new Config(); // created lazily, on first use return Config._instance; } } console.log("same object:", Config.instance() === Config.instance() ? "True" : "False");public class Main { public static void main(String[] args) { System.out.println("same object: " + (Config.instance() == Config.instance() ? "True" : "False")); } } final class Config { private Config() {} private static class Holder { // initialisation-on-demand holder: lazy and thread-safe static final Config INSTANCE = new Config(); } static Config instance() { return Holder.INSTANCE; } }<?php final class Config { private static ?Config $instance = null; private function __construct() {} public static function instance(): Config { return self::$instance ??= new Config(); // created lazily, on first use } } echo "same object: ", Config::instance() === Config::instance() ? "True" : "False", "\n";Key Points
- Private constructor + static accessor.
- Global access hides dependencies and leaks state.
- Thread-safe lazy init in Java: holder idiom or enum.
- Prefer one injected instance or container singleton scope.
Deep dive: Design Patterns, Explained Simply: Singleton
-
Most asked What is the Adapter pattern? Give an example.
Summary
Adapter wraps a class that has an incompatible interface and translates calls to the interface your code expects. For example, a
LegacyBillingAdapterimplements yourPaymentProvider.charge(cents)by converting cents to a dollar string and calling the legacysubmit().Longer Answer
The client sees only the target interface; all translation (method names, units, result shapes, error types) lives in the adapter. That keeps vendor-specific details out of business logic and makes it easy to switch or add providers.
Real examples: Laravel's filesystem adapters (local, S3, FTP behind one API), Java's
InputStreamReaderandArrays.asList(), DB-API drivers in Python.Code Example
class LegacyBilling: # third-party: amount as a dollar string def submit(self, amount): return {"ref": f"LB-{amount}"} class BillingAdapter: # our interface: charge(cents) -> id def __init__(self, legacy): self.legacy = legacy def charge(self, cents): return self.legacy.submit(f"{cents // 100}.{cents % 100:02d}")["ref"] print(BillingAdapter(LegacyBilling()).charge(1250))class LegacyBilling { // third-party: amount as a dollar string submit(amount: string) { return { ref: `LB-${amount}` }; } } interface PaymentProvider { charge(cents: number): string; } class BillingAdapter implements PaymentProvider { constructor(private legacy: LegacyBilling) {} charge(cents: number) { return this.legacy.submit(`${Math.floor(cents / 100)}.${String(cents % 100).padStart(2, "0")}`).ref; } } console.log(new BillingAdapter(new LegacyBilling()).charge(1250));import java.util.Map; public class Main { public static void main(String[] args) { System.out.println(new BillingAdapter(new LegacyBilling()).charge(1250)); } } class LegacyBilling { // third-party: amount as a dollar string Map<String, String> submit(String amount) { return Map.of("ref", "LB-" + amount); } } interface PaymentProvider { String charge(int cents); } class BillingAdapter implements PaymentProvider { private final LegacyBilling legacy; BillingAdapter(LegacyBilling legacy) { this.legacy = legacy; } public String charge(int cents) { return legacy.submit(String.format("%d.%02d", cents / 100, cents % 100)).get("ref"); } }<?php class LegacyBilling { // third-party: amount as a dollar string public function submit(string $amount): array { return ["ref" => "LB-$amount"]; } } interface PaymentProvider { public function charge(int $cents): string; } class BillingAdapter implements PaymentProvider { public function __construct(private LegacyBilling $legacy) {} public function charge(int $cents): string { return $this->legacy->submit(sprintf("%d.%02d", intdiv($cents, 100), $cents % 100))["ref"]; } } echo (new BillingAdapter(new LegacyBilling()))->charge(1250), "\n";Key Points
- Translates an incompatible interface to the expected one.
- Client stays unchanged.
- Ideal for third-party and legacy code.
- Keeps vendor types out of the domain.
Deep dive: Design Patterns, Explained Simply: Adapter
-
Most asked What is the Decorator pattern? How is it different from inheritance?
Summary
Decorator wraps an object in another object with the same interface that adds behaviour before or after delegating. Unlike inheritance, decorators are combined at runtime and stack in any order, so you don't need a subclass for every combination of features.
Longer Answer
Typical uses are cross-cutting concerns: logging, caching, retries, metrics, authorization, compression.
LoggingFetcher(CachingFetcher(RetryFetcher(http)))composes three features without changingHttpFetcher. Order matters: logging outside the cache logs every call; inside it, only real fetches.Java I/O streams are the textbook example; Python's
@decoratorapplies the same idea to functions; HTTP middleware is a decorator chain around a handler.Code Example
class Http: def get(self, url): return f"<{url}>" class Logged: # decorator: same get(), adds logging def __init__(self, inner): self.inner = inner def get(self, url): print(f"GET {url}") return self.inner.get(url) class Cached: # decorator: same get(), adds caching def __init__(self, inner): self.inner, self.cache = inner, {} def get(self, url): if url not in self.cache: self.cache[url] = self.inner.get(url) return self.cache[url] client = Cached(Logged(Http())) # logging inside the cache: only real fetches are logged print(client.get("/a")) print(client.get("/a"))interface Client { get(url: string): string; } class Http implements Client { get(url: string) { return `<${url}>`; } } class Logged implements Client { // decorator: same get(), adds logging constructor(private inner: Client) {} get(url: string) { console.log(`GET ${url}`); return this.inner.get(url); } } class Cached implements Client { // decorator: same get(), adds caching private cache = new Map<string, string>(); constructor(private inner: Client) {} get(url: string) { if (!this.cache.has(url)) this.cache.set(url, this.inner.get(url)); return this.cache.get(url)!; } } const client = new Cached(new Logged(new Http())); // logging inside the cache: only real fetches are logged console.log(client.get("/a")); console.log(client.get("/a"));import java.util.HashMap; import java.util.Map; public class Main { public static void main(String[] args) { Client client = new Cached(new Logged(new Http())); // logging inside the cache: only real fetches are logged System.out.println(client.get("/a")); System.out.println(client.get("/a")); } } interface Client { String get(String url); } class Http implements Client { public String get(String url) { return "<" + url + ">"; } } class Logged implements Client { // decorator: same get(), adds logging private final Client inner; Logged(Client inner) { this.inner = inner; } public String get(String url) { System.out.println("GET " + url); return inner.get(url); } } class Cached implements Client { // decorator: same get(), adds caching private final Client inner; private final Map<String, String> cache = new HashMap<>(); Cached(Client inner) { this.inner = inner; } public String get(String url) { return cache.computeIfAbsent(url, inner::get); } }<?php interface Client { public function get(string $url): string; } class Http implements Client { public function get(string $url): string { return "<$url>"; } } class Logged implements Client { // decorator: same get(), adds logging public function __construct(private Client $inner) {} public function get(string $url): string { echo "GET $url\n"; return $this->inner->get($url); } } class Cached implements Client { // decorator: same get(), adds caching private array $cache = []; public function __construct(private Client $inner) {} public function get(string $url): string { return $this->cache[$url] ??= $this->inner->get($url); } } $client = new Cached(new Logged(new Http())); // logging inside the cache: only real fetches are logged $first = $client->get("/a"); echo $first, "\n"; echo $client->get("/a"), "\n";Key Points
- Same interface, wraps and delegates.
- Adds behaviour without changing the class.
- Stackable at runtime; order matters.
- Java I/O streams, Python decorators, middleware.
Deep dive: Design Patterns, Explained Simply: Decorator
-
Most asked Explain the Observer pattern and give examples from real frameworks.
Summary
Observer lets a subject notify any number of subscribers when something happens, without knowing what they do. Subscribers register and unregister at runtime. Examples: DOM
addEventListener, Node.jsEventEmitter, RxJS, Laravel events and model observers, Django signals, Spring application events.Longer Answer
It decouples the source of an event from its reactions: order placement doesn't import email, analytics and inventory code; they subscribe. Decide how ordering and errors are handled (one failing observer shouldn't break the subject unless it must), and always unsubscribe to avoid memory leaks.
Across processes or services, use Pub/Sub with a broker instead of in-process observers.
Code Example
class Events: def __init__(self): self.subscribers = [] def subscribe(self, fn): self.subscribers.append(fn) def publish(self, order_id): for fn in self.subscribers: fn(order_id) events = Events() events.subscribe(lambda oid: print(f"email: receipt for {oid}")) events.subscribe(lambda oid: print(f"stock: reserve for {oid}")) events.publish(42)class Events { private subscribers: ((orderId: number) => void)[] = []; subscribe(fn: (orderId: number) => void) { this.subscribers.push(fn); } publish(orderId: number) { for (const fn of this.subscribers) fn(orderId); } } const events = new Events(); events.subscribe((oid) => console.log(`email: receipt for ${oid}`)); events.subscribe((oid) => console.log(`stock: reserve for ${oid}`)); events.publish(42);import java.util.ArrayList; import java.util.List; import java.util.function.IntConsumer; public class Main { public static void main(String[] args) { Events events = new Events(); events.subscribe(oid -> System.out.println("email: receipt for " + oid)); events.subscribe(oid -> System.out.println("stock: reserve for " + oid)); events.publish(42); } } class Events { private final List<IntConsumer> subscribers = new ArrayList<>(); void subscribe(IntConsumer fn) { subscribers.add(fn); } void publish(int orderId) { subscribers.forEach(fn -> fn.accept(orderId)); } }<?php class Events { private array $subscribers = []; public function subscribe(callable $fn): void { $this->subscribers[] = $fn; } public function publish(int $orderId): void { foreach ($this->subscribers as $fn) $fn($orderId); } } $events = new Events(); $events->subscribe(fn($oid) => print("email: receipt for $oid\n")); $events->subscribe(fn($oid) => print("stock: reserve for $oid\n")); $events->publish(42);Key Points
- Subject notifies subscribers it doesn't know.
- Subscribe/unsubscribe at runtime.
- Handle observer errors and ordering deliberately.
- Unsubscribe to avoid leaks.
Deep dive: Design Patterns, Explained Simply: Observer
-
Most asked Explain the Strategy pattern with an example.
Summary
Strategy puts interchangeable algorithms behind one interface; a context holds one strategy and delegates to it, so the algorithm can be chosen at runtime without
if/else. Shipping cost rules (standard, express, pickup) are a typical example.Longer Answer
Each strategy is testable alone, and new ones are added without editing the context (Open/Closed). Selection usually happens once, through configuration or a lookup table. In Python, TypeScript, modern Java and PHP a strategy is often just a function or lambda; comparators passed to sort functions are strategies.
Code Example
STRATEGIES = { # strategies as plain functions in a lookup table "standard": lambda subtotal: 0 if subtotal >= 5000 else 499, "express": lambda subtotal: 1299, "pickup": lambda subtotal: 0, } def total(subtotal, method): return subtotal + STRATEGIES[method](subtotal) for method in ["standard", "express", "pickup"]: print(method, total(4200, method))type Shipping = (subtotal: number) => number; const strategies: Record<string, Shipping> = { // strategies as plain functions in a lookup table standard: (subtotal) => (subtotal >= 5000 ? 0 : 499), express: () => 1299, pickup: () => 0, }; const total = (subtotal: number, method: string) => subtotal + strategies[method](subtotal); for (const method of ["standard", "express", "pickup"]) console.log(method, total(4200, method));import java.util.List; import java.util.Map; import java.util.function.IntUnaryOperator; public class Main { static final Map<String, IntUnaryOperator> STRATEGIES = Map.of( // strategies as lambdas in a lookup table "standard", subtotal -> subtotal >= 5000 ? 0 : 499, "express", subtotal -> 1299, "pickup", subtotal -> 0); static int total(int subtotal, String method) { return subtotal + STRATEGIES.get(method).applyAsInt(subtotal); } public static void main(String[] args) { for (String method : List.of("standard", "express", "pickup")) System.out.println(method + " " + total(4200, method)); } }<?php $strategies = [ // strategies as closures in a lookup table "standard" => fn(int $subtotal) => $subtotal >= 5000 ? 0 : 499, "express" => fn(int $subtotal) => 1299, "pickup" => fn(int $subtotal) => 0, ]; $total = fn(int $subtotal, string $method) => $subtotal + $strategies[$method]($subtotal); foreach (["standard", "express", "pickup"] as $method) echo "$method ", $total(4200, $method), "\n";Key Points
- Interchangeable algorithms behind one interface.
- Context delegates; no if/else.
- Often just a function.
- Pick via lookup table or configuration.
Deep dive: Design Patterns, Explained Simply: Strategy
-
Most asked What is the difference between the Strategy and State patterns?
Summary
Both delegate to an interchangeable object, but in Strategy the client chooses the algorithm and strategies don't know each other, while in State the state objects themselves trigger transitions to other states as the object's lifecycle progresses.
Longer Answer
Strategy answers "how should this be done?" (shipping cost: standard, express); the choice is usually made once from configuration or user input. State answers "what is allowed right now?" (an order that is pending, then paid, then shipped); behaviour changes as the object moves through states, and the states know their successors.
Their class diagrams look almost identical; the difference is who changes the delegate and why.
Key Points
- Same structure, different intent.
- Strategy: chosen by the client, independent.
- State: transitions driven by states.
- Strategy = how; State = what's allowed now.
Deep dive: Design Patterns, Explained Simply: Choosing the Right Pattern
-
Most asked Compare Adapter, Decorator, Proxy and Facade.
Summary
All four wrap something. Adapter gives an existing object a different interface so it fits; Decorator keeps the same interface and adds behaviour (stackable); Proxy keeps the same interface and controls access (lazy, permissions, remote); Facade offers a new, simpler interface over a whole subsystem.
Longer Answer
The code shape can be nearly identical (a class holding another and delegating), so the distinction is intent:
Pattern Interface Intent Adapter different make it fit Decorator same add behaviour Proxy same control access Facade new, simpler, over many classes simplify Naming the wrapper by its intent tells the next reader why it exists.
Key Points
- Adapter: fit a different interface.
- Decorator: add behaviour, same interface.
- Proxy: control access, same interface.
- Facade: simplify a subsystem.
Deep dive: Design Patterns, Explained Simply: Choosing the Right Pattern
-
Most asked What is the difference between Observer and Pub/Sub?
Summary
In Observer, the subject holds direct references to its observers and usually notifies them synchronously in the same process. In Pub/Sub, publishers and subscribers only know a broker and topic names; delivery is typically asynchronous, can cross processes and services, and failures are isolated with retries and dead-letter queues.
Longer Answer
Observer is simpler and immediate (UI events, in-process domain events). Pub/Sub decouples further and scales across services (SNS/SQS, Kafka, RabbitMQ, Google Pub/Sub), at the cost of eventual consistency, duplicate deliveries (subscribers must be idempotent) and harder debugging.
Key Points
- Observer: direct references, synchronous, in-process.
- Pub/Sub: broker and topics, asynchronous.
- Pub/Sub isolates failures but brings eventual consistency.
- Make Pub/Sub subscribers idempotent.
Deep dive: Design Patterns, Explained Simply: Pub/Sub and the Event Bus
-
Most asked What is the Repository pattern, and when is it worth using?
Summary
A repository is a collection-like interface (
add,get,overdue_invoices()) that hides how domain objects are stored. It's worth using when business logic is mixed with queries and you want it testable with in-memory implementations or decoupled from the storage technology.Longer Answer
Repositories come from Martin Fowler's Patterns of Enterprise Application Architecture and are central to Domain-Driven Design and hexagonal architecture. Spring Data repositories are a framework version.
They add little in simple CRUD apps where an Active Record ORM already provides query methods, and generic repositories that just mirror the ORM (
find_by(any_field)) are a common anti-pattern. Make methods domain-specific.Key Points
- Collection-like interface over storage.
- Business logic testable with in-memory repos.
- Domain-specific methods, not generic wrappers.
- Often unnecessary for simple CRUD.
Deep dive: Design Patterns, Explained Simply: Repository and Unit of Work
-
Most asked When should you NOT use a design pattern?
Summary
When the problem it solves isn't present yet: one implementation, no variation, or simple code that's easy to change later. Apply YAGNI and the rule of three, prefer language features (functions, named arguments, generators), and add the pattern when the variation becomes real.
Longer Answer
Each pattern adds indirection that readers must follow. A strategy interface with one implementation, a factory that only calls
new, or a builder for a two-field object costs more than it saves. Refactoring toward a pattern later, with tests in place, is cheap when the need is real.Key Points
- No problem, no pattern.
- YAGNI and the rule of three.
- Language features often replace patterns.
- Refactor toward patterns when variation appears.
Deep dive: Design Patterns, Explained Simply: Anti-Patterns and Over-Engineering
-
Most asked Scenario: your checkout supports card and PayPal, and the business adds a new payment provider every quarter. How would you design it?
Summary
Use Strategy: a
PaymentMethodinterface with one implementation per provider, chosen through a lookup table or factory from the customer's choice. Wrap each vendor SDK in an Adapter so provider quirks stay out of checkout. New providers become new classes plus a registration line.Longer Answer
Checkout depends only on
PaymentMethod.charge(). Each provider's adapter handles units, idempotency keys, error mapping and webhooks for that vendor. Tests use a fake payment method. Selection lives in one place (a map from method name to implementation), so noif/elsegrows in checkout.Key Points
- Strategy for interchangeable providers.
- Adapter per vendor SDK.
- Lookup table/factory for selection.
- Checkout unchanged when providers are added.
Deep dive: Design Patterns, Explained Simply: Capstone: Refactor a Tangled Checkout
-
Most asked What are the three categories of Gang of Four design patterns?
Summary
Creational patterns deal with object creation (Factory Method, Abstract Factory, Builder, Prototype, Singleton), structural patterns with composing objects (Adapter, Bridge, Composite, Decorator, Facade, Flyweight, Proxy), and behavioral patterns with how objects communicate and share responsibilities (Chain of Responsibility, Command, Iterator, Mediator, Memento, Observer, State, Strategy, Template Method, Visitor, Interpreter).
Longer Answer
The 23 classic patterns split 5 / 7 / 11 across the three families. A quick way to remember them: creational answers "how do I create objects flexibly?", structural answers "how do I fit objects together?", and behavioral answers "how do objects divide work and talk to each other?".
Modern codebases add patterns beyond the original 23, such as dependency injection, repository, middleware and pub/sub, which build on the same ideas.
Key Points
- Creational: 5 patterns about creating objects.
- Structural: 7 patterns about composing objects.
- Behavioral: 11 patterns about communication and responsibility.
Deep dive: Design Patterns, Explained Simply: What a Design Pattern Is
-
Most asked Explain the Liskov Substitution Principle with an example.
Summary
Subtypes must be usable wherever their parent type is expected, without breaking the parent's promises. The classic violation:
Square extends Rectangle, where setting the height also changes the width, so code expecting area 5 x 4 = 20 gets 16.Longer Answer
LSP is about behavioural compatibility, not vocabulary. A square "is a" rectangle in geometry, but not a correct subtype of a mutable
Rectangleclass whose contract says width and height change independently.Signs of violations: overridden methods that throw "not supported", stricter preconditions, weaker results, and callers that need
instanceofchecks. Fixes include modelling by shared behaviour (an immutableShapewitharea()), or composition instead of inheritance.Key Points
- Subtypes must keep the parent's contract.
- Square/Rectangle is the classic violation.
- Smells: 'not supported' overrides, instanceof checks.
- Fix by modelling shared behaviour or composing.
-
Most asked What is the Facade pattern?
Summary
Facade provides one simple, task-focused interface to a complex subsystem. An
OrderFacade.place_order()coordinates inventory, payments, shipping and email in the right order, so callers don't repeat that sequence.Longer Answer
The subsystem classes keep working and remain available for advanced use; the facade is a convenience, not a wall. It reduces duplication and coupling to the subsystem's details, and it is a natural boundary when layering a system.
Risk: a facade that does everything becomes a god object. Keep facades focused on one area.
Key Points
- Simple front door to a complex subsystem.
- Coordinates calls in the right order.
- Subsystem still usable directly.
- Keep it focused to avoid a god object.
Deep dive: Design Patterns, Explained Simply: Facade
-
Most asked How would you implement undo/redo with the Command pattern?
Summary
Make each action an object with
execute()andundo()that stores what it needs to reverse itself. An invoker runs commands and pushes them on a history stack; undo pops and callsundo()(moving the command to a redo stack); any new command clears the redo stack.Longer Answer
The invoker never knows what a command does, so new actions (bold, paste) are new command classes. Commands are also how job queues work (a serialised command run later by a worker), and how macros and audit logs are built.
For actions with external side effects (an email, a card charge), "undo" means a compensating action (a correction, a refund), not restoring previous state.
Key Points
- Command = action as an object with execute/undo.
- History stack for undo, redo stack for redo.
- New command clears redo.
- External side effects need compensating actions.
Deep dive: Design Patterns, Explained Simply: Command
-
Most asked Compare Factory Method, Abstract Factory and Builder.
Summary
Factory Method creates one product, with the concrete class chosen by a subclass; Abstract Factory creates a matching family of related products through one factory object; Builder creates one complex product step by step with validation at the end.
Longer Answer
Choose Factory Method (or a simple factory) when the question is "which class?"; Abstract Factory when several objects must match (theme widgets, vendor drivers); Builder when the question is "how do I assemble this complex object readably?" (many optional parts, invalid combinations).
They combine: an abstract factory can use builders internally, and a builder can be returned by a factory.
Key Points
- Factory Method: which class, via subclass.
- Abstract Factory: matching families.
- Builder: step-by-step assembly.
Deep dive: Design Patterns, Explained Simply: Choosing the Right Pattern
-
Most asked What is MVC, and which design patterns does it combine?
Summary
Model-View-Controller separates data and business rules (model), presentation (view) and input handling (controller). Classic MVC combines Observer (views observe the model), Strategy (the controller is the view's strategy for handling input) and Composite (nested views).
Longer Answer
Web frameworks (Rails, Laravel, Django's MTV, Spring MVC, ASP.NET MVC) adapt it: a controller handles an HTTP request, works with models, and renders a view or JSON. Variants include MVP and MVVM (data binding in Vue, Angular, WPF).
The common pitfall is fat controllers; move business logic into models or services.
Key Points
- Model, view, controller separation.
- Built from Observer, Strategy and Composite.
- Web MVC is a request-based adaptation.
- Avoid fat controllers.
-
Most asked What is an anti-pattern? Name a few common ones.
Summary
An anti-pattern is a common response to a recurring problem that looks reasonable but usually causes more harm than good. Common ones: the god object, singleton used as a global variable, service locator, golden hammer (one favourite solution for everything), speculative generality, copy-paste programming and the anemic domain model.
Longer Answer
Recognising them matters as much as knowing patterns: a 3,000-line
OrderManager(god object) orcontainer.get()calls inside business classes (service locator) are more common than missing patterns. Each has a known remedy: split by responsibility, inject dependencies, start from the problem, wait for the second case.Key Points
- Looks right, causes harm.
- God object, singleton-as-global, service locator.
- Golden hammer, speculative generality.
- Each has a known remedy.
Deep dive: Design Patterns, Explained Simply: Anti-Patterns and Over-Engineering
-
Most asked Scenario: when an order is placed, several systems (email, inventory, analytics, loyalty) must react, and more will be added. How do you design it?
Summary
Publish an
OrderPlacedevent and let each system subscribe: Observer in-process, or Pub/Sub through a broker if the reactions are slow, unreliable or in other services. Order placement no longer knows its consumers.Longer Answer
With a broker (SQS/SNS, Kafka, RabbitMQ), use the transactional outbox pattern so the event is published only if the order is committed, make subscribers idempotent (at-least-once delivery), and add dead-letter queues. In-process observers are fine for fast, reliable reactions in a monolith.
Key Points
- Event + subscribers.
- Observer in-process, Pub/Sub across services.
- Outbox for reliable publishing.
- Idempotent subscribers and DLQs.
Deep dive: Design Patterns, Explained Simply: Pub/Sub and the Event Bus
- What does the Single Responsibility Principle mean in practice?
- How do you apply the Open/Closed Principle?
- What kinds of Proxy are there, and what does each do?
- What is the State pattern? When would you use it?
- What is the Circuit Breaker pattern?
- How do you safely refactor existing code toward a design pattern?
- What is the Interface Segregation Principle?
- What does "program to an interface, not an implementation" mean?
- What are coupling and cohesion, and how do design patterns affect them?
- When would you use the Abstract Factory pattern?
- What is the Prototype pattern, and what is the main pitfall when implementing it?
- What is the Bridge pattern?
- Explain the Composite pattern with a real-world example.
- Explain Chain of Responsibility and give a web-development example.
- What is the Iterator pattern, and how do modern languages support it?
- What is the Template Method pattern?
- When would you use a Decorator instead of subclassing?
- Template Method or Strategy: how do you choose?
- Is HTTP middleware Chain of Responsibility or Decorator?
- What is the Unit of Work pattern?
- What is CQRS, and how does it relate to the Command pattern?
- Which design patterns do you see in React?
- Which design patterns do Laravel and Spring use internally?
- What is the Middleware (pipeline) pattern used for?
- How would you refactor a god object?
- Why is a service locator considered worse than dependency injection?
- Scenario: you need undo/redo in a drawing or text editor. Which pattern would you use?
- Scenario: a third-party SDK you must use has a completely different interface from the rest of your code. What do you do?
- Scenario: a class has a constructor with ten parameters, many optional. How would you improve it?
- Scenario: you need to add caching and logging to an existing service without modifying it. How?
- Scenario: every method of an `Order` class starts with a switch on `status`. How would you refactor it?
- How would you explain to a junior developer how to choose the right design pattern?
- What is the difference between Active Record and Data Mapper?
- Which design patterns does the Java standard library use?
- Which design patterns do Python features replace or simplify?
- Which design patterns do you use in PHP and Laravel day to day?
- Which design patterns are common in TypeScript and Node.js code?
- How do design patterns relate to testability?
- What is the Flyweight pattern, and when is it worth using?
- What is the Mediator pattern?
- What is the Memento pattern?
- What is the Visitor pattern, and what is its main trade-off?
- What is the Interpreter pattern?
- Command or Memento for undo: which would you use?
- How is Mediator different from Observer?
- What is the difference between Bridge and Adapter?
- How do Composite and Decorator differ, given both use recursive composition?
- When would you use a simple factory, and when a dependency injection container?
- What is the Null Object pattern, and when can it be harmful?
- What is the Specification pattern?
- Scenario: product categories contain products and subcategories to any depth, and you need totals per category. Which pattern fits?
- What is the Object Pool pattern, and where do you meet it?
- What is the Strangler Fig pattern, and how do design patterns help with it?
- Is it a good idea to name classes after patterns, such as `PaymentStrategy` or `UserFactory`?