Design Patterns
Creational, structural, and behavioral patterns
Singleton: implementation and thread-safety concerns
How do you implement a thread-safe Singleton? What are the concerns?
Singleton: Ensure a class has only one instance and provide global access.
Naive Implementation (Not Thread-Safe):
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) { // Race condition!
instance = new Singleton();
}
return instance;
}
}
Problem: Two threads can both see instance == null and create two instances.
Thread-Safe Implementations:
1. Synchronized Method (Simple but slow)
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
2. Double-Checked Locking
public class Singleton {
private static volatile Singleton instance;
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
Note: volatile is required to prevent instruction reordering.
3. Initialization-on-Demand Holder (Recommended)
public class Singleton {
private Singleton() {}
private static class Holder {
static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return Holder.INSTANCE;
}
}
Thread-safe by JVM class loading mechanism, lazy initialization.
4. Enum Singleton (Best)
public enum Singleton {
INSTANCE;
public void doSomething() { }
}
Thread-safe, serialization-safe, reflection-proof.
Concerns:
1. Global state - Hard to test
2. Hidden dependencies - Not explicit
3. Violation of SRP - Creation + functionality
4. Difficult to mock - Use DI instead
Key Points to Look For:
- Knows thread-safety issues
- Can implement multiple approaches
- Understands when not to use
Follow-up: Why is Singleton often considered an anti-pattern?
Factory Method vs Abstract Factory
What is the difference between Factory Method and Abstract Factory patterns?
Factory Method: Defines an interface for creating an object, but lets subclasses decide which class to instantiate.
// Factory Method
abstract class Dialog {
abstract Button createButton(); // Factory method
void render() {
Button btn = createButton();
btn.onClick(this::closeDialog);
btn.render();
}
}
class WindowsDialog extends Dialog {
Button createButton() {
return new WindowsButton();
}
}
class MacDialog extends Dialog {
Button createButton() {
return new MacButton();
}
}
Abstract Factory: Provides an interface for creating families of related objects.
// Abstract Factory
interface GUIFactory {
Button createButton();
Checkbox createCheckbox();
TextBox createTextBox();
}
class WindowsFactory implements GUIFactory {
Button createButton() { return new WindowsButton(); }
Checkbox createCheckbox() { return new WindowsCheckbox(); }
TextBox createTextBox() { return new WindowsTextBox(); }
}
class MacFactory implements GUIFactory {
Button createButton() { return new MacButton(); }
Checkbox createCheckbox() { return new MacCheckbox(); }
TextBox createTextBox() { return new MacTextBox(); }
}
// Client
class Application {
private GUIFactory factory;
void createUI() {
Button btn = factory.createButton();
Checkbox cb = factory.createCheckbox();
}
}
Key Differences:
| Aspect | Factory Method | Abstract Factory |
|---|---|---|
| Focus | Single product | Family of products |
| Implementation | Method in class | Separate factory class |
| Extension | Subclass creator | Implement factory interface |
| Use case | One object type varies | Multiple related objects |
When to Use:
Factory Method:
- Single object creation varies by context
- Framework where client code extends base
Abstract Factory:
- Multiple related products
- Product families must be consistent
- Switching entire product lines
Key Points to Look For:
- Clear distinction between patterns
- Knows when to use each
- Can implement both
Follow-up: How do these patterns support the Open/Closed Principle?
Builder pattern: when is it overkill?
When should you use the Builder pattern? When is it overkill?
Builder: Separate construction of complex objects from their representation.
When Builder Helps:
// Without Builder - confusing constructor
User user = new User("John", "Doe", "john@email.com", 30,
"123 Street", "City", "12345", true, false, "premium");
// With Builder - readable
User user = User.builder()
.firstName("John")
.lastName("Doe")
.email("john@email.com")
.age(30)
.address(Address.builder()
.street("123 Street")
.city("City")
.zip("12345")
.build())
.active(true)
.premium(true)
.build();
Use Builder When:
1. Many parameters (4+ constructor arguments)
2. Optional parameters (many combinations)
3. Immutable objects with many fields
4. Complex construction (multi-step process)
5. Fluent interface desired for readability
Builder is Overkill When:
1. Few parameters (2-3 fields)
// Overkill for simple objects
Point point = Point.builder().x(10).y(20).build();
// Just use constructor
Point point = new Point(10, 20);
- All parameters required
// If everything is mandatory, constructor is clearer
User user = new User(name, email); // Can't forget anything
- Mutable objects
// Just use setters
User user = new User();
user.setName("John");
user.setEmail("john@email.com");
Implementation:
public class User {
private final String name;
private final String email;
private final int age;
private User(Builder builder) {
this.name = builder.name;
this.email = builder.email;
this.age = builder.age;
}
public static Builder builder() {
return new Builder();
}
public static class Builder {
private String name;
private String email;
private int age;
public Builder name(String name) {
this.name = name;
return this;
}
public Builder email(String email) {
this.email = email;
return this;
}
public Builder age(int age) {
this.age = age;
return this;
}
public User build() {
// Validate required fields
Objects.requireNonNull(name);
Objects.requireNonNull(email);
return new User(this);
}
}
}
Key Points to Look For:
- Knows when pattern is beneficial
- Recognizes overkill scenarios
- Can implement builder
Follow-up: How does Lombok's @Builder annotation work?
Prototype: deep vs shallow copy
Explain the Prototype pattern. What's the difference between deep and shallow copy?
Prototype: Create new objects by cloning an existing prototype instance.
Use Case:
When object creation is expensive, or you need copies with slight modifications.
interface Prototype {
Prototype clone();
}
class Document implements Prototype {
private String content;
private List<Page> pages;
public Document clone() {
Document copy = new Document();
copy.content = this.content;
copy.pages = new ArrayList<>(this.pages); // Shallow!
return copy;
}
}
Shallow Copy:
Copies object references, not the objects themselves.
Document copy = original.clone(); // Shallow
copy.pages == original.pages; // Same list reference!
copy.pages.add(newPage); // Affects original too!
Original: Copy:
+----------+ +----------+
| content | | content | (copied)
| pages ───────→ [Page1, Page2, Page3]
+----------+ +----------+
Deep Copy:
Recursively copies all objects.
class Document implements Prototype {
public Document deepClone() {
Document copy = new Document();
copy.content = this.content;
copy.pages = new ArrayList<>();
for (Page page : this.pages) {
copy.pages.add(page.deepClone()); // Clone each
}
return copy;
}
}
Original: Copy:
+----------+ +----------+
| content | | content |
| pages ───┼──→ [P1, P2] | pages ───┼──→ [P1', P2']
+----------+ +----------+
Java Cloning:
class Person implements Cloneable {
private String name;
private Address address;
@Override
protected Person clone() throws CloneNotSupportedException {
Person copy = (Person) super.clone(); // Shallow
copy.address = this.address.clone(); // Make it deep
return copy;
}
}
Copy Constructor Alternative:
class Person {
public Person(Person other) {
this.name = other.name;
this.address = new Address(other.address);
}
}
When to Use:
- Object creation is expensive
- Need many similar objects
- Runtime configuration of objects
Key Points to Look For:
- Understands shallow vs deep
- Knows cloning pitfalls
- Can implement both types
Follow-up: What problems can arise with Java's Cloneable interface?
Object Pool pattern for expensive resources
What is the Object Pool pattern? When and how would you use it?
Object Pool: Reuse expensive objects instead of creating/destroying them repeatedly.
Use Cases:
- Database connections
- Thread pools
- Socket connections
- Large buffers
Implementation:
class ConnectionPool {
private final List<Connection> available = new ArrayList<>();
private final List<Connection> inUse = new ArrayList<>();
private final int maxSize;
private final ConnectionFactory factory;
public synchronized Connection acquire() {
if (available.isEmpty()) {
if (inUse.size() < maxSize) {
Connection conn = factory.create();
inUse.add(conn);
return conn;
}
// Wait or throw exception
throw new PoolExhaustedException();
}
Connection conn = available.remove(available.size() - 1);
inUse.add(conn);
return conn;
}
public synchronized void release(Connection conn) {
inUse.remove(conn);
if (conn.isValid()) {
available.add(conn);
} else {
conn.close(); // Don't return broken connections
}
}
}
Thread-Safe Implementation:
class ThreadSafePool<T> {
private final BlockingQueue<T> pool;
private final Supplier<T> factory;
public ThreadSafePool(int size, Supplier<T> factory) {
this.pool = new ArrayBlockingQueue<>(size);
this.factory = factory;
// Pre-populate
for (int i = 0; i < size; i++) {
pool.offer(factory.get());
}
}
public T acquire() throws InterruptedException {
return pool.take(); // Blocks if empty
}
public void release(T obj) {
pool.offer(obj);
}
}
With Try-With-Resources:
class PooledConnection implements AutoCloseable {
private final Connection conn;
private final ConnectionPool pool;
@Override
public void close() {
pool.release(conn); // Return to pool, don't close
}
}
// Usage
try (PooledConnection conn = pool.acquire()) {
// Use connection
} // Automatically returned to pool
Considerations:
1. Validation - Check objects before reuse
2. Reset state - Clear object state on release
3. Sizing - Too small = contention, too large = waste
4. Timeout - Don't wait forever
5. Leaks - Track unreturned objects
Key Points to Look For:
- Knows when pooling helps
- Handles thread-safety
- Considers validation/reset
Follow-up: How does HikariCP implement connection pooling?
Dependency Injection as a pattern
How is Dependency Injection a design pattern? What are the different types?
Dependency Injection: Technique where dependencies are provided ("injected") rather than created by the class itself.
Without DI:
class OrderService {
private PaymentGateway gateway = new StripeGateway(); // Hardcoded!
private EmailService email = new SmtpEmailService(); // Hardcoded!
}
With DI:
class OrderService {
private final PaymentGateway gateway;
private final EmailService email;
// Dependencies injected
OrderService(PaymentGateway gateway, EmailService email) {
this.gateway = gateway;
this.email = email;
}
}
Types of Injection:
1. Constructor Injection (Preferred)
class Service {
private final Repository repo;
Service(Repository repo) {
this.repo = repo;
}
}
- Dependencies explicit
- Object always valid
- Supports immutability
2. Setter Injection
class Service {
private Repository repo;
void setRepository(Repository repo) {
this.repo = repo;
}
}
- Optional dependencies
- Runtime reconfiguration
- Can be in invalid state
3. Interface Injection
interface RepositoryAware {
void setRepository(Repository repo);
}
class Service implements RepositoryAware {
public void setRepository(Repository repo) { }
}
- Rarely used
- Framework-specific
4. Field Injection (Avoid)
class Service {
@Inject
private Repository repo; // Reflection magic
}
- Hard to test
- Hidden dependencies
- No immutability
Benefits:
1. Testability - Inject mocks
2. Flexibility - Swap implementations
3. Explicit dependencies - Clear requirements
4. Decoupling - Depends on interface
DI Containers:
// Spring
@Component
class OrderService {
@Autowired
public OrderService(PaymentGateway gateway) { }
}
// Guice
class AppModule extends AbstractModule {
@Override
void configure() {
bind(PaymentGateway.class).to(StripeGateway.class);
}
}
Key Points to Look For:
- Knows different injection types
- Prefers constructor injection
- Understands testing benefit
Follow-up: When would you use manual DI vs a DI framework?
Structural Patterns
Adapter: class vs object adapter
What is the Adapter pattern? What's the difference between class and object adapters?
Adapter: Convert interface of a class into another interface clients expect.
Problem:
// Existing class with incompatible interface
class OldPaymentSystem {
void makePayment(String account, int cents) { }
}
// Expected interface
interface PaymentProcessor {
void pay(PaymentRequest request);
}
Object Adapter (Composition):
class PaymentAdapter implements PaymentProcessor {
private OldPaymentSystem legacy; // Composition
PaymentAdapter(OldPaymentSystem legacy) {
this.legacy = legacy;
}
@Override
public void pay(PaymentRequest request) {
legacy.makePayment(
request.getAccountNumber(),
(int) (request.getAmount() * 100)
);
}
}
Class Adapter (Inheritance):
class PaymentAdapter extends OldPaymentSystem implements PaymentProcessor {
@Override
public void pay(PaymentRequest request) {
makePayment( // Inherited method
request.getAccountNumber(),
(int) (request.getAmount() * 100)
);
}
}
Comparison:
| Aspect | Object Adapter | Class Adapter |
|---|---|---|
| Mechanism | Composition | Inheritance |
| Flexibility | Can adapt subclasses | Single class only |
| Override behavior | Cannot override | Can override |
| Multiple adaptees | Yes | No (single inheritance) |
| Language support | All OOP languages | Needs multiple inheritance |
When to Use:
- Object Adapter: More flexible, preferred in most cases
- Class Adapter: When you need to override adaptee behavior
Real Examples:
- InputStreamReader adapts InputStream to Reader
- Arrays.asList() adapts array to List interface
- JDBC drivers adapt database protocols
Key Points to Look For:
- Knows both approaches
- Prefers object adapter
- Can give real examples
Follow-up: How does Adapter differ from Facade?
Decorator vs Inheritance for extending behavior
When would you use the Decorator pattern instead of inheritance to extend behavior?
Decorator: Attach additional responsibilities dynamically without subclassing.
Problem with Inheritance:
// Explosion of subclasses
class Coffee { }
class CoffeeWithMilk extends Coffee { }
class CoffeeWithSugar extends Coffee { }
class CoffeeWithMilkAndSugar extends Coffee { }
class CoffeeWithMilkAndSugarAndVanilla extends Coffee { }
// Combinatorial explosion!
Decorator Solution:
interface Coffee {
double cost();
String description();
}
class BasicCoffee implements Coffee {
public double cost() { return 2.0; }
public String description() { return "Coffee"; }
}
abstract class CoffeeDecorator implements Coffee {
protected Coffee coffee;
CoffeeDecorator(Coffee coffee) { this.coffee = coffee; }
}
class MilkDecorator extends CoffeeDecorator {
MilkDecorator(Coffee coffee) { super(coffee); }
public double cost() { return coffee.cost() + 0.5; }
public String description() { return coffee.description() + " + Milk"; }
}
class SugarDecorator extends CoffeeDecorator {
SugarDecorator(Coffee coffee) { super(coffee); }
public double cost() { return coffee.cost() + 0.2; }
public String description() { return coffee.description() + " + Sugar"; }
}
// Usage - combine freely at runtime
Coffee order = new SugarDecorator(
new MilkDecorator(
new BasicCoffee()
)
);
// "Coffee + Milk + Sugar" costs $2.70
When to Use Decorator:
1. Runtime composition - Combine behaviors dynamically
2. Multiple independent features - Avoid subclass explosion
3. Open/Closed Principle - Add features without modifying existing
4. Single Responsibility - Each decorator does one thing
When to Use Inheritance:
1. True IS-A relationship
2. Override/change behavior (not add)
3. Fixed hierarchy known at compile time
4. Access to protected members needed
Java I/O Example:
InputStream in = new BufferedInputStream(
new FileInputStream("file.txt")
);
// FileInputStream → BufferedInputStream → InputStream
Key Points to Look For:
- Understands composition over inheritance
- Knows decorator enables runtime combination
- Can identify explosion problem
Follow-up: What's the difference between Decorator and Proxy?
Facade: simplifying complex subsystems
What is the Facade pattern? Give an example of when you'd use it.
Facade: Provide a simplified interface to a complex subsystem.
Problem - Complex Subsystem:
// Client needs to know all these classes
VideoFile file = new VideoFile(filename);
CodecFactory factory = new CodecFactory();
Codec codec = factory.extract(file);
BitrateReader reader = new BitrateReader();
byte[] raw = reader.read(file, codec);
AudioMixer mixer = new AudioMixer();
byte[] audio = mixer.fix(raw);
// ... more complexity
Facade Solution:
class VideoConverter {
public File convert(String filename, String format) {
// Hides all complexity
VideoFile file = new VideoFile(filename);
Codec codec = CodecFactory.extract(file);
byte[] raw = BitrateReader.read(file, codec);
byte[] result = AudioMixer.fix(raw);
return new File(result, format);
}
}
// Simple client usage
VideoConverter converter = new VideoConverter();
File mp4 = converter.convert("movie.avi", "mp4");
Real Examples:
1. JDBC Facade
// Without facade - complex
Connection conn = DriverManager.getConnection(url);
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql);
// Handle results, close resources...
// With facade (e.g., JdbcTemplate)
List<User> users = jdbcTemplate.query(sql, rowMapper);
2. SLF4J (Logging Facade)
// Hides Log4j, Logback, java.util.logging complexity
Logger logger = LoggerFactory.getLogger(MyClass.class);
logger.info("Simple logging");
Benefits:
1. Simplicity - Easy-to-use interface
2. Decoupling - Client doesn't depend on subsystem details
3. Layer organization - Clear entry point to subsystem
4. Testability - Mock the facade
Not a Facade:
- Facade doesn't prevent access to subsystem
- It's an additional interface, not a restriction
Key Points to Look For:
- Understands simplification purpose
- Knows it's about interface, not hiding
- Can give practical examples
Follow-up: How does Facade differ from Adapter?
Proxy types: virtual, protection, remote
What are the different types of Proxy patterns and their use cases?
Proxy: Provide a surrogate for another object to control access.
1. Virtual Proxy (Lazy Loading)
Delays expensive object creation until needed.
interface Image {
void display();
}
class RealImage implements Image {
private byte[] data;
RealImage(String filename) {
loadFromDisk(filename); // Expensive!
}
}
class ImageProxy implements Image {
private RealImage realImage;
private String filename;
ImageProxy(String filename) {
this.filename = filename; // No loading yet
}
public void display() {
if (realImage == null) {
realImage = new RealImage(filename); // Load on first use
}
realImage.display();
}
}
2. Protection Proxy (Access Control)
Controls access based on permissions.
class DocumentProxy implements Document {
private Document realDoc;
private User user;
public void edit(String content) {
if (!user.hasPermission("EDIT")) {
throw new AccessDeniedException();
}
realDoc.edit(content);
}
public String read() {
if (!user.hasPermission("READ")) {
throw new AccessDeniedException();
}
return realDoc.read();
}
}
3. Remote Proxy (Network)
Represents object in different address space.
// Local proxy for remote service
class OrderServiceProxy implements OrderService {
private String serviceUrl;
public Order getOrder(Long id) {
// Makes HTTP call to remote service
return httpClient.get(serviceUrl + "/orders/" + id, Order.class);
}
}
4. Caching Proxy
Caches results of expensive operations.
class CachingUserServiceProxy implements UserService {
private UserService realService;
private Map<Long, User> cache = new HashMap<>();
public User findById(Long id) {
return cache.computeIfAbsent(id, realService::findById);
}
}
5. Logging/Monitoring Proxy
class LoggingProxy implements Service {
private Service real;
public void doWork() {
log.info("Before doWork");
long start = System.nanoTime();
real.doWork();
log.info("doWork took {}ms", (System.nanoTime() - start) / 1_000_000);
}
}
Java Dynamic Proxy:
InvocationHandler handler = (proxy, method, args) -> {
log.info("Calling " + method.getName());
return method.invoke(realObject, args);
};
Service proxy = (Service) Proxy.newProxyInstance(
Service.class.getClassLoader(),
new Class[]{Service.class},
handler
);
Key Points to Look For:
- Knows multiple proxy types
- Can give use case for each
- Understands dynamic proxies
Follow-up: How do Spring AOP proxies work?
Composite pattern for tree structures
When would you use the Composite pattern? Explain with an example.
Composite: Compose objects into tree structures and treat individual objects and compositions uniformly.
Use Case: Part-whole hierarchies where clients should treat leaves and composites the same way.
// Component
interface FileSystemItem {
String getName();
long getSize();
void print(String indent);
}
// Leaf
class File implements FileSystemItem {
private String name;
private long size;
public String getName() { return name; }
public long getSize() { return size; }
public void print(String indent) {
System.out.println(indent + name + " (" + size + " bytes)");
}
}
// Composite
class Directory implements FileSystemItem {
private String name;
private List<FileSystemItem> children = new ArrayList<>();
public void add(FileSystemItem item) { children.add(item); }
public void remove(FileSystemItem item) { children.remove(item); }
public String getName() { return name; }
public long getSize() {
return children.stream()
.mapToLong(FileSystemItem::getSize)
.sum();
}
public void print(String indent) {
System.out.println(indent + name + "/");
for (FileSystemItem child : children) {
child.print(indent + " ");
}
}
}
Usage:
Directory root = new Directory("root");
Directory docs = new Directory("docs");
docs.add(new File("readme.txt", 1024));
docs.add(new File("notes.txt", 512));
root.add(docs);
root.add(new File("config.json", 256));
root.print("");
// Output:
// root/
// docs/
// readme.txt (1024 bytes)
// notes.txt (512 bytes)
// config.json (256 bytes)
root.getSize(); // 1792 (sum of all files)
Real Examples:
- UI component hierarchies (Container has Components)
- Organization charts (Org has Departments has Employees)
- Menu systems (Menu has MenuItems and subMenus)
- Graphics (Group has Shapes and Groups)
When to Use:
- Part-whole hierarchies
- Clients should ignore difference between compositions and individuals
- Recursive structures
Key Points to Look For:
- Understands uniform treatment benefit
- Can implement component/leaf/composite
- Knows real-world examples
Follow-up: How would you implement an operation that only makes sense for composites?
Bridge: separating abstraction from implementation
What problem does the Bridge pattern solve? Give an example.
Bridge: Decouple abstraction from implementation so both can vary independently.
Problem Without Bridge:
// Class explosion with multiple dimensions
class Shape { }
class RedCircle extends Shape { }
class BlueCircle extends Shape { }
class RedSquare extends Shape { }
class BlueSquare extends Shape { }
// Adding new shape = 2 new classes (each color)
// Adding new color = N new classes (each shape)
Bridge Solution:
// Implementation hierarchy
interface Color {
void applyColor();
}
class RedColor implements Color {
public void applyColor() { System.out.println("Red"); }
}
class BlueColor implements Color {
public void applyColor() { System.out.println("Blue"); }
}
// Abstraction hierarchy
abstract class Shape {
protected Color color; // Bridge!
Shape(Color color) { this.color = color; }
abstract void draw();
}
class Circle extends Shape {
Circle(Color color) { super(color); }
void draw() {
System.out.print("Circle in ");
color.applyColor();
}
}
class Square extends Shape {
Square(Color color) { super(color); }
void draw() {
System.out.print("Square in ");
color.applyColor();
}
}
// Usage
Shape redCircle = new Circle(new RedColor());
Shape blueSquare = new Square(new BlueColor());
Two Independent Hierarchies:
Abstraction: Implementation:
Shape Color
├── Circle ├── Red
├── Square ├── Blue
└── Triangle └── Green
Without Bridge: 3 × 3 = 9 classes
With Bridge: 3 + 3 = 6 classes
Real Example - Remote Controls:
// Abstraction
abstract class RemoteControl {
protected Device device;
void togglePower() {
if (device.isEnabled()) device.disable();
else device.enable();
}
abstract void channelUp();
}
// Refined Abstraction
class AdvancedRemote extends RemoteControl {
void mute() { device.setVolume(0); }
}
// Implementation
interface Device {
void enable();
void disable();
void setVolume(int volume);
}
class TV implements Device { }
class Radio implements Device { }
// Any remote works with any device
RemoteControl remote = new AdvancedRemote(new TV());
When to Use:
- Multiple dimensions of variation
- Avoid cartesian product of classes
- Need runtime binding of implementation
- Share implementation among abstractions
Key Points to Look For:
- Understands two-hierarchy concept
- Can explain class explosion problem
- Knows when pattern applies
Follow-up: How does Bridge differ from Adapter?
Flyweight for memory optimization
What is the Flyweight pattern? When would you use it?
Flyweight: Share common state among many objects to reduce memory usage.
Intrinsic vs Extrinsic State:
- Intrinsic: Shared, immutable, context-independent
- Extrinsic: Unique, passed by client, context-dependent
Example: Text Editor
// Without Flyweight - each character is an object
class Character {
char value;
String font; // Repeated!
int fontSize; // Repeated!
Color color; // Repeated!
int positionX;
int positionY;
}
// "Hello" = 5 objects with mostly repeated data
// With Flyweight
class CharacterStyle { // Flyweight (shared)
final String font; // Intrinsic
final int fontSize; // Intrinsic
final Color color; // Intrinsic
}
class CharacterContext { // Extrinsic state
char value;
int positionX;
int positionY;
CharacterStyle style; // Reference to shared flyweight
}
class StyleFactory {
private Map<String, CharacterStyle> styles = new HashMap<>();
CharacterStyle getStyle(String font, int size, Color color) {
String key = font + size + color;
return styles.computeIfAbsent(key,
k -> new CharacterStyle(font, size, color));
}
}
Game Example - Trees in Forest:
// Flyweight - shared tree type data
class TreeType {
final String name;
final Color color;
final Texture texture; // Large image data
void draw(int x, int y) {
// Draw texture at position
}
}
// Context - unique per tree
class Tree {
int x, y;
TreeType type; // Shared flyweight
void draw() { type.draw(x, y); }
}
class Forest {
List<Tree> trees;
TreeTypeFactory factory;
void plantTree(int x, int y, String name) {
TreeType type = factory.getTreeType(name); // Reuse!
trees.add(new Tree(x, y, type));
}
}
Memory Comparison:
1000 trees without Flyweight:
- 1000 × (coords + name + color + 10MB texture) ≈ 10GB
1000 trees with Flyweight:
- 3 TreeTypes × 10MB = 30MB (shared)
- 1000 × (coords + reference) ≈ 20KB
- Total ≈ 30MB
When to Use:
- Large number of similar objects
- Objects share significant state
- Extrinsic state can be computed/passed
- Object identity doesn't matter
Key Points to Look For:
- Understands intrinsic vs extrinsic
- Can identify shareable state
- Knows memory benefit
Follow-up: How does Java's String pool use the Flyweight pattern?
Behavioral Patterns
Observer pattern and event-driven design
Explain the Observer pattern. How does it enable event-driven design?
Observer: Define a one-to-many dependency so when one object changes state, all dependents are notified.
Implementation:
// Subject (Observable)
interface Subject {
void attach(Observer o);
void detach(Observer o);
void notifyObservers();
}
// Observer
interface Observer {
void update(Event event);
}
// Concrete Subject
class Stock implements Subject {
private List<Observer> observers = new ArrayList<>();
private double price;
public void setPrice(double price) {
this.price = price;
notifyObservers();
}
public void attach(Observer o) { observers.add(o); }
public void detach(Observer o) { observers.remove(o); }
public void notifyObservers() {
Event event = new PriceChangeEvent(this, price);
observers.forEach(o -> o.update(event));
}
}
// Concrete Observers
class TradingBot implements Observer {
public void update(Event event) {
if (event instanceof PriceChangeEvent) {
double price = ((PriceChangeEvent) event).getPrice();
if (price < threshold) buy();
}
}
}
class AlertSystem implements Observer {
public void update(Event event) {
sendNotification(event.toString());
}
}
Usage:
Stock apple = new Stock("AAPL");
apple.attach(new TradingBot());
apple.attach(new AlertSystem());
apple.attach(new PriceChart());
apple.setPrice(150.0); // All observers notified
Event-Driven Benefits:
1. Loose coupling - Subject doesn't know observer types
2. Dynamic subscription - Add/remove at runtime
3. Broadcast - Multiple receivers for one event
4. Extensibility - Add observers without changing subject
Modern Java:
// PropertyChangeSupport
class Stock {
private PropertyChangeSupport pcs = new PropertyChangeSupport(this);
public void addListener(PropertyChangeListener l) {
pcs.addPropertyChangeListener(l);
}
public void setPrice(double price) {
double old = this.price;
this.price = price;
pcs.firePropertyChange("price", old, price);
}
}
Common Uses:
- GUI event handling (button clicks)
- MVC pattern (model notifies views)
- Pub/sub messaging
- Real-time data feeds
Key Points to Look For:
- Knows push vs pull notification
- Understands decoupling benefit
- Can implement basic observer
Follow-up: What's the difference between Observer and Pub/Sub?
Strategy vs State pattern differences
What's the difference between Strategy and State patterns? They look similar.
Both encapsulate behavior in separate classes, but for different purposes:
Strategy: Choose algorithm at runtime. Client selects strategy.
State: Change behavior when internal state changes. Object manages transitions.
Strategy Example:
interface PaymentStrategy {
void pay(double amount);
}
class CreditCardPayment implements PaymentStrategy {
void pay(double amount) { /* charge card */ }
}
class PayPalPayment implements PaymentStrategy {
void pay(double amount) { /* use PayPal API */ }
}
class ShoppingCart {
private PaymentStrategy strategy;
void setPaymentStrategy(PaymentStrategy s) {
this.strategy = s; // Client chooses
}
void checkout(double total) {
strategy.pay(total);
}
}
// Usage - client explicitly chooses
cart.setPaymentStrategy(new CreditCardPayment());
cart.checkout(100);
State Example:
interface OrderState {
void next(Order order);
void cancel(Order order);
}
class PendingState implements OrderState {
void next(Order order) {
order.setState(new ProcessingState()); // Auto-transition
}
void cancel(Order order) {
order.setState(new CancelledState());
}
}
class ProcessingState implements OrderState {
void next(Order order) {
order.setState(new ShippedState());
}
void cancel(Order order) {
throw new IllegalStateException("Cannot cancel processing order");
}
}
class Order {
private OrderState state = new PendingState();
void setState(OrderState state) { this.state = state; }
void proceed() { state.next(this); } // State changes itself
}
// Usage - transitions happen automatically
order.proceed(); // Pending → Processing
order.proceed(); // Processing → Shipped
Key Differences:
| Aspect | Strategy | State |
|---|---|---|
| Who changes | Client explicitly | Object/State internally |
| Awareness | Strategies don't know each other | States know about transitions |
| Purpose | Select algorithm | Model state machine |
| Transitions | N/A | Defined by states |
| Typical use | Payment methods, sorting | Order status, UI modes |
When to Use:
Strategy:
- Multiple algorithms for same task
- Client should choose
- Algorithms are interchangeable
State:
- Object behavior depends on state
- State transitions are well-defined
- Replacing long if/switch on state
Key Points to Look For:
- Knows semantic difference
- Identifies who manages changes
- Can give examples for each
Follow-up: How would you implement an undo/redo feature using these patterns?
Command pattern: undo/redo implementation
How does the Command pattern enable undo/redo functionality?
Command: Encapsulate a request as an object, allowing parameterization, queuing, and undo.
Basic Structure:
interface Command {
void execute();
void undo();
}
Text Editor Example:
class InsertTextCommand implements Command {
private Document doc;
private String text;
private int position;
InsertTextCommand(Document doc, int position, String text) {
this.doc = doc;
this.position = position;
this.text = text;
}
public void execute() {
doc.insert(position, text);
}
public void undo() {
doc.delete(position, text.length());
}
}
class DeleteTextCommand implements Command {
private Document doc;
private String deletedText; // Store for undo
private int position;
public void execute() {
deletedText = doc.getText(position, length); // Save
doc.delete(position, length);
}
public void undo() {
doc.insert(position, deletedText); // Restore
}
}
Command History Manager:
class CommandHistory {
private Stack<Command> undoStack = new Stack<>();
private Stack<Command> redoStack = new Stack<>();
void executeCommand(Command cmd) {
cmd.execute();
undoStack.push(cmd);
redoStack.clear(); // Clear redo on new action
}
void undo() {
if (undoStack.isEmpty()) return;
Command cmd = undoStack.pop();
cmd.undo();
redoStack.push(cmd);
}
void redo() {
if (redoStack.isEmpty()) return;
Command cmd = redoStack.pop();
cmd.execute();
undoStack.push(cmd);
}
}
Composite Command (Macro):
class MacroCommand implements Command {
private List<Command> commands = new ArrayList<>();
void add(Command cmd) { commands.add(cmd); }
public void execute() {
commands.forEach(Command::execute);
}
public void undo() {
// Undo in reverse order
for (int i = commands.size() - 1; i >= 0; i--) {
commands.get(i).undo();
}
}
}
Benefits:
1. Undo/Redo - Commands remember how to reverse
2. Logging - Persist commands for audit
3. Queuing - Execute later, batch processing
4. Transactional - Rollback on failure
Key Points to Look For:
- Stores state for undo
- Manages command history
- Handles redo correctly
Follow-up: How would you implement persistent undo (survives restart)?
Template Method vs Strategy
When would you use Template Method instead of Strategy?
Both allow varying parts of an algorithm, but differ in how:
Template Method: Inheritance-based. Algorithm skeleton in base class, steps overridden in subclasses.
abstract class DataProcessor {
// Template method - final, can't override
public final void process() {
readData();
processData(); // Hook - subclass implements
writeData();
}
protected abstract void processData(); // Subclass varies this
private void readData() { /* fixed step */ }
private void writeData() { /* fixed step */ }
}
class CSVProcessor extends DataProcessor {
@Override
protected void processData() {
// CSV-specific processing
}
}
class XMLProcessor extends DataProcessor {
@Override
protected void processData() {
// XML-specific processing
}
}
Strategy: Composition-based. Entire algorithm encapsulated and injected.
interface ProcessingStrategy {
void process(Data data);
}
class DataProcessor {
private ProcessingStrategy strategy;
DataProcessor(ProcessingStrategy strategy) {
this.strategy = strategy;
}
public void process() {
Data data = readData();
strategy.process(data); // Delegated to strategy
writeData(data);
}
}
class CSVStrategy implements ProcessingStrategy { }
class XMLStrategy implements ProcessingStrategy { }
// Can swap at runtime
processor.setStrategy(new XMLStrategy());
Comparison:
| Aspect | Template Method | Strategy |
|---|---|---|
| Mechanism | Inheritance | Composition |
| Algorithm control | Base class | Strategy object |
| Change timing | Compile-time | Runtime |
| Steps overridden | Parts (hooks) | Entire algorithm |
| Class count | Subclass per variant | Strategy per variant |
Use Template Method When:
- Algorithm structure is fixed
- Only certain steps vary
- Want to enforce algorithm skeleton
- Framework hooks (e.g., Servlet lifecycle)
Use Strategy When:
- Need runtime switching
- Entire algorithm varies
- Want to avoid inheritance
- Multiple strategies in same object
Key Points to Look For:
- Knows inheritance vs composition difference
- Understands when each is appropriate
- Can implement both
Follow-up: How does the Hollywood Principle relate to Template Method?
Iterator: external vs internal iteration
What's the difference between external and internal iteration?
Iterator: Provide a way to access elements sequentially without exposing underlying representation.
External Iteration:
Client controls iteration explicitly.
// External - client controls loop
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
Iterator<String> iterator = names.iterator();
while (iterator.hasNext()) {
String name = iterator.next();
System.out.println(name);
}
// Or enhanced for-loop (still external)
for (String name : names) {
System.out.println(name);
}
Internal Iteration:
Collection controls iteration, client provides action.
// Internal - collection controls loop
names.forEach(name -> System.out.println(name));
// With streams
names.stream()
.filter(name -> name.length() > 3)
.forEach(System.out::println);
Comparison:
| Aspect | External | Internal |
|---|---|---|
| Control | Client has loop | Collection has loop |
| Flexibility | More control | Less control |
| Parallelism | Manual | Easy (parallelStream) |
| Code style | Imperative | Declarative |
| Early exit | Easy (break) | Harder |
Custom Iterator:
class TreeIterator<T> implements Iterator<T> {
private Stack<Node<T>> stack = new Stack<>();
TreeIterator(Node<T> root) {
pushLeft(root);
}
private void pushLeft(Node<T> node) {
while (node != null) {
stack.push(node);
node = node.left;
}
}
public boolean hasNext() {
return !stack.isEmpty();
}
public T next() {
Node<T> node = stack.pop();
pushLeft(node.right);
return node.value;
}
}
// Usage
for (Integer value : tree) { // Uses iterator
System.out.println(value);
}
When to Use:
External:
- Need fine-grained control
- Early termination (break)
- Multiple iterators simultaneously
- Modify during iteration
Internal:
- Cleaner, declarative code
- Potential parallelism
- No early exit needed
Key Points to Look For:
- Knows both styles
- Understands trade-offs
- Can implement custom iterator
Follow-up: How do fail-fast iterators work?
Chain of Responsibility: request handling
Explain Chain of Responsibility pattern with a practical example.
Chain of Responsibility: Pass request along a chain of handlers. Each handler decides to process or pass to next.
Example: HTTP Request Processing
abstract class Handler {
private Handler next;
void setNext(Handler next) {
this.next = next;
}
void handle(Request request) {
if (canHandle(request)) {
process(request);
} else if (next != null) {
next.handle(request);
}
}
abstract boolean canHandle(Request request);
abstract void process(Request request);
}
class AuthenticationHandler extends Handler {
boolean canHandle(Request req) { return true; } // Always runs
void process(Request req) {
if (!isAuthenticated(req)) {
req.setResponse(401, "Unauthorized");
return; // Stop chain
}
// Pass to next
if (next != null) next.handle(req);
}
}
class AuthorizationHandler extends Handler {
void process(Request req) {
if (!hasPermission(req)) {
req.setResponse(403, "Forbidden");
return;
}
if (next != null) next.handle(req);
}
}
class ValidationHandler extends Handler {
void process(Request req) {
if (!isValid(req.getBody())) {
req.setResponse(400, "Bad Request");
return;
}
if (next != null) next.handle(req);
}
}
class BusinessLogicHandler extends Handler {
void process(Request req) {
// Actually handle the request
req.setResponse(200, "OK");
}
}
Usage:
Handler chain = new AuthenticationHandler();
chain.setNext(new AuthorizationHandler())
.setNext(new ValidationHandler())
.setNext(new BusinessLogicHandler());
chain.handle(request);
Real Examples:
- Servlet filters
- Middleware in Express.js
- Exception handling (catch blocks)
- Event bubbling in UI
- Logging levels
Variants:
1. Stop on handle - First matching handler processes
2. All handlers run - Every handler gets chance
3. Transform and pass - Each handler modifies request
Benefits:
- Decouple sender from receivers
- Dynamic chain configuration
- Single responsibility per handler
Key Points to Look For:
- Understands chain traversal
- Knows stopping vs continuing
- Can give real examples
Follow-up: How do middleware chains in web frameworks use this pattern?
Mediator: reducing coupling between objects
What problem does the Mediator pattern solve? Give an example.
Mediator: Define an object that encapsulates how a set of objects interact, promoting loose coupling.
Problem Without Mediator:
// Direct coupling - every component knows others
class Button {
private TextBox textBox;
private Label label;
private Checkbox checkbox;
void click() {
textBox.clear();
label.setText("Clicked");
checkbox.uncheck();
}
}
// Adding new component requires changing all others
With Mediator:
interface DialogMediator {
void notify(Component sender, String event);
}
class RegistrationDialog implements DialogMediator {
private Button submitBtn;
private TextBox nameField;
private TextBox emailField;
private Checkbox termsBox;
public void notify(Component sender, String event) {
if (sender == termsBox && event.equals("check")) {
submitBtn.setEnabled(termsBox.isChecked());
}
if (sender == submitBtn && event.equals("click")) {
if (validateForm()) {
submitForm();
}
}
if (sender == nameField && event.equals("change")) {
validateName();
}
}
}
// Components only know mediator
abstract class Component {
protected DialogMediator mediator;
void setMediator(DialogMediator m) { mediator = m; }
}
class Button extends Component {
void click() {
mediator.notify(this, "click");
}
}
class Checkbox extends Component {
void check() {
mediator.notify(this, "check");
}
}
Without vs With Mediator:
Without: With:
A ←→ B A → M ← B
↕ ↕ ↑ ↑
C ←→ D C → M ← D
Connections: 6 Connections: 4
Real Examples:
- Air traffic control (planes communicate via tower)
- Chat room (users communicate via room)
- GUI forms (components via form controller)
- Event bus / Message broker
Trade-offs:
- Pro: Reduces coupling between components
- Pro: Centralizes complex logic
- Con: Mediator can become God Object
- Con: Single point of failure
Mediator vs Observer:
- Observer: One-to-many notification
- Mediator: Many-to-many coordination
Key Points to Look For:
- Understands coupling reduction
- Can identify God Object risk
- Knows when pattern applies
Follow-up: How do you prevent the Mediator from becoming a God Class?
Memento for state snapshots
How does the Memento pattern work? When would you use it?
Memento: Capture and externalize an object's internal state so it can be restored later, without violating encapsulation.
Components:
- Originator: Object whose state we save
- Memento: Stores originator's state
- Caretaker: Manages mementos (undo history)
Implementation:
// Memento - stores state
class EditorMemento {
private final String content;
private final int cursorPosition;
EditorMemento(String content, int cursor) {
this.content = content;
this.cursorPosition = cursor;
}
String getContent() { return content; }
int getCursor() { return cursorPosition; }
}
// Originator - creates and restores from memento
class TextEditor {
private String content = "";
private int cursorPosition = 0;
void type(String text) {
content = content.substring(0, cursorPosition) +
text +
content.substring(cursorPosition);
cursorPosition += text.length();
}
EditorMemento save() {
return new EditorMemento(content, cursorPosition);
}
void restore(EditorMemento memento) {
content = memento.getContent();
cursorPosition = memento.getCursor();
}
}
// Caretaker - manages history
class History {
private Stack<EditorMemento> states = new Stack<>();
void push(EditorMemento memento) {
states.push(memento);
}
EditorMemento pop() {
return states.pop();
}
}
Usage:
TextEditor editor = new TextEditor();
History history = new History();
editor.type("Hello");
history.push(editor.save()); // Save state
editor.type(" World");
history.push(editor.save()); // Save state
editor.type("!!!");
// content = "Hello World!!!"
editor.restore(history.pop());
// content = "Hello World"
editor.restore(history.pop());
// content = "Hello"
Encapsulation:
// Nested class maintains encapsulation
class TextEditor {
private String content;
class Memento { // Only Editor can access internals
private final String savedContent;
private Memento(String content) { this.savedContent = content; }
}
Memento save() { return new Memento(content); }
void restore(Memento m) { content = m.savedContent; }
}
Use Cases:
- Undo/Redo functionality
- Checkpoint/rollback in transactions
- Game save states
- Versioning systems
Key Points to Look For:
- Understands encapsulation preservation
- Knows three participants
- Can implement basic version
Follow-up: How would you implement incremental mementos for large objects?
Visitor pattern trade-offs
What are the trade-offs of the Visitor pattern? When is it appropriate?
Visitor: Represent an operation to be performed on elements of an object structure. Add new operations without changing element classes.
Implementation:
// Element interface
interface DocumentElement {
void accept(Visitor visitor);
}
class Heading implements DocumentElement {
String text;
int level;
void accept(Visitor v) { v.visit(this); }
}
class Paragraph implements DocumentElement {
String content;
void accept(Visitor v) { v.visit(this); }
}
class Image implements DocumentElement {
String url;
void accept(Visitor v) { v.visit(this); }
}
// Visitor interface
interface Visitor {
void visit(Heading heading);
void visit(Paragraph paragraph);
void visit(Image image);
}
// Concrete visitors - new operations
class HTMLExportVisitor implements Visitor {
void visit(Heading h) {
output("<h" + h.level + ">" + h.text + "</h" + h.level + ">");
}
void visit(Paragraph p) { output("<p>" + p.content + "</p>"); }
void visit(Image i) { output("<img src='" + i.url + "'/>"); }
}
class WordCountVisitor implements Visitor {
private int count = 0;
void visit(Heading h) { count += countWords(h.text); }
void visit(Paragraph p) { count += countWords(p.content); }
void visit(Image i) { /* images have no words */ }
}
Usage:
List<DocumentElement> document = getDocument();
Visitor htmlExporter = new HTMLExportVisitor();
for (DocumentElement element : document) {
element.accept(htmlExporter);
}
Trade-offs:
| Pro | Con |
|---|---|
| Easy to add new operations | Hard to add new element types |
| Related behavior in one class | Breaks encapsulation (public methods) |
| Accumulate state across visits | Double dispatch complexity |
| External to elements | Requires element modification |
When to Use:
- Object structure rarely changes
- Many distinct operations needed
- Operations don't fit in element classes
- Traversal logic should be external
When to Avoid:
- Element types change frequently (each new type = change all visitors)
- Only 1-2 operations needed
- Simple hierarchy
- Elements can handle operations themselves
Double Dispatch:
// 1. element.accept(visitor) - dispatch on element type
// 2. visitor.visit(this) - dispatch on visitor type
Key Points to Look For:
- Understands adding operations vs types trade-off
- Knows double dispatch mechanism
- Can identify appropriate use cases
Follow-up: How would you handle adding a new element type without changing all visitors?
Pattern Selection
How to choose the right pattern?
How do you decide which design pattern to use for a given problem?
Pattern Selection Process:
1. Identify the Problem Category
| Problem | Patterns to Consider |
|---|---|
| Object creation complexity | Factory, Builder, Prototype |
| Algorithm variation | Strategy, Template Method |
| State-dependent behavior | State, Strategy |
| Structure adaptation | Adapter, Decorator, Proxy |
| Complex object structure | Composite, Flyweight |
| Object communication | Observer, Mediator, Command |
| Traversing structures | Iterator, Visitor |
2. Ask Key Questions
Creation Problems:
- "Does object creation vary?" → Factory
- "Many optional parameters?" → Builder
- "Expensive to create, need copies?" → Prototype
Behavioral Problems:
- "Behavior changes at runtime?" → Strategy
- "Object state affects behavior?" → State
- "Need to undo/queue operations?" → Command
- "One-to-many notifications?" → Observer
Structural Problems:
- "Incompatible interfaces?" → Adapter
- "Add responsibilities dynamically?" → Decorator
- "Control access to object?" → Proxy
- "Simplify complex subsystem?" → Facade
3. Consider Trade-offs
Pattern vs Complexity:
- Simple problem → No pattern needed
- Medium problem → Single pattern
- Complex problem → Multiple patterns
Pattern vs Flexibility:
- High flexibility → More abstraction
- Low flexibility → Simpler code
4. Red Flags - When NOT to Use Patterns
- "I want to use a pattern" (pattern hunting)
- Only one concrete implementation likely
- Simple conditional would suffice
- Team doesn't know the pattern
- Problem doesn't match pattern intent
Decision Tree Example:
Need to create objects?
├── Yes, complex construction? → Builder
├── Yes, family of objects? → Abstract Factory
├── Yes, single type varies? → Factory Method
└── No
Need to add behavior?
├── Yes, at runtime? → Decorator
├── Yes, different algorithms? → Strategy
└── No
Need to adapt interfaces?
├── Yes, incompatible? → Adapter
├── Yes, simplify? → Facade
└── No
Key Points to Look For:
- Problem-first approach
- Considers alternatives
- Knows when not to use
Follow-up: Can you give an example where you chose NOT to use a pattern?
Anti-patterns: what to avoid
What are common anti-patterns in software design?
Anti-patterns are common solutions that seem good but cause problems.
1. God Object / God Class
// Everything in one class
class Application {
void handleUserLogin() { }
void processPayment() { }
void sendEmail() { }
void generateReport() { }
void backupDatabase() { }
}
Fix: Single Responsibility, extract classes
2. Singleton Abuse
// Global state everywhere
UserSession.getInstance().getUser();
Config.getInstance().getValue();
Logger.getInstance().log();
Fix: Dependency Injection
3. Golden Hammer
Using same solution for every problem.
"We use microservices for everything"
"Everything is a Factory"
Fix: Choose tools for the job
4. Cargo Cult Programming
// Copy-paste without understanding
// "We always do it this way"
try {
// code
} catch (Exception e) {
// What was this for again?
}
Fix: Understand before using
5. Lava Flow
Dead code that no one dares to remove.
// TODO: Remove after migration (2018)
void oldMethod() {
// Is this still used???
}
Fix: Test coverage, brave deletion
6. Spaghetti Code
Tangled control flow, no clear structure.
Fix: Refactoring, separation of concerns
7. Copy-Paste Programming
// Same code in 5 places, slightly different
Fix: DRY, extract common logic
8. Premature Optimization
// Complex caching before profiling
// "This might be slow"
Fix: Profile first, optimize after
9. Magic Numbers/Strings
if (status == 4) { } // What is 4?
if (type.equals("A")) { } // What is A?
Fix: Named constants, enums
10. Feature Envy
Method more interested in another class's data.
void calculatePrice(Order order) {
return order.getQuantity() * order.getProduct().getPrice() *
(1 - order.getCustomer().getDiscount());
}
Fix: Tell Don't Ask, move to Order
Key Points to Look For:
- Knows multiple anti-patterns
- Can explain why they're harmful
- Knows remedies
Follow-up: How do you address anti-patterns in legacy code?
Patterns in modern frameworks
Give examples of design patterns used in popular frameworks.
Spring Framework:
| Pattern | Usage |
|---|---|
| Singleton | Default bean scope |
| Factory | BeanFactory, ApplicationContext |
| Proxy | AOP, @Transactional |
| Template Method | JdbcTemplate, RestTemplate |
| Strategy | Resource loaders |
| Observer | ApplicationEvent, @EventListener |
| Decorator | BeanPostProcessor |
// Template Method in JdbcTemplate
jdbcTemplate.query(sql, (rs, rowNum) -> {
// You provide the mapping
return new User(rs.getString("name"));
});
React:
| Pattern | Usage |
|---|---|
| Composite | Component tree |
| Observer | State updates, hooks |
| Strategy | Render props |
| Decorator | Higher-Order Components |
| Flyweight | Virtual DOM reconciliation |
// Composite - components contain components
<App>
<Header />
<Main>
<Sidebar />
<Content />
</Main>
</App>
Java Collections:
| Pattern | Usage |
|---|---|
| Iterator | Collection.iterator() |
| Factory | Collections.emptyList() |
| Decorator | Collections.synchronized*() |
| Strategy | Comparator |
// Decorator pattern
List<String> syncList = Collections.synchronizedList(new ArrayList<>());
// Strategy pattern
list.sort(Comparator.comparing(User::getName));
Servlet/Web:
| Pattern | Usage |
|---|---|
| Chain of Responsibility | Filter chain |
| Front Controller | DispatcherServlet |
| MVC | Spring MVC |
JUnit:
| Pattern | Usage |
|---|---|
| Template Method | @Before, @After hooks |
| Command | Test as runnable command |
| Composite | Test suites |
Key Points to Look For:
- Knows patterns in frameworks they use
- Can explain how pattern is applied
- Recognizes patterns in code
Follow-up: How does understanding these patterns help when debugging?