What are Design Patterns?
GoF (Gang of Four — Gamma, Helm, Johnson, Vlissides, authors of "Design Patterns", 1994).
Junior Level
Design patterns are solutions documented in books and articles that have been repeatedly applied in real projects and proven to work.
Why: No need to design a solution from scratch. Saves 2-5 hours per task because the pattern is already tested and understood by other developers.
Simple analogy: Imagine building a house. You don’t need to reinvent how to build a roof or foundation every time — there are standard blueprints that have already proven effective. Patterns are such “standard blueprints” for code.
Why needed:
- Don’t reinvent the wheel
- Use solutions that already work
- Communicate with colleagues using a common language (“we need a Singleton here”)
Example:
// Singleton pattern — only one instance of the class
public class Logger {
private static Logger instance = new Logger();
private Logger() {}
public static Logger getInstance() { return instance; }
public void log(String message) {
System.out.println(message);
}
}
// Usage
Logger.getInstance().log("Hello!");
When to use:
- When facing a typical problem
- When code becomes complex and confusing
- When you need to make code clearer for other developers
Middle Level
Pattern Levels
| Level | Examples | Where Applied | | —————— | ———————————— | ——————— | | Language Idioms | Try-with-resources, Optional, Streams | Java syntax | | GoF Patterns | Strategy, Decorator, Observer, Factory | Class interaction | GoF (Gang of Four — Gamma, Helm, Johnson, Vlissides, authors of “Design Patterns”, 1994). | Enterprise | Repository, Unit of Work, Data Mapper | Data handling | | Cloud/Distributed | Circuit Breaker, Saga, CQRS | Microservices |
Main Categories
1. Creational
- Help create objects
- Examples: Singleton, Factory, Builder
- Why: Hide the object creation process
2. Structural
- Help compose classes and objects
- Examples: Adapter, Decorator, Proxy
- Why: Make different interfaces compatible
3. Behavioral
- Describe interaction between objects
- Examples: Strategy, Observer, Chain of Responsibility
- Why: Manage algorithms and data flow
Patterns in Modern Java
Many classic patterns became simpler with Java 8-21:
// Classic Strategy (many classes)
public interface PaymentStrategy { void pay(); }
public class CreditCardStrategy implements PaymentStrategy { ... }
public class PayPalStrategy implements PaymentStrategy { ... }
// Modern Java (lambdas)
public void processPayment(UnaryOperator<BigDecimal> paymentStrategy) {
BigDecimal result = paymentStrategy.apply(amount);
}
// Usage
processPayment(amount -> amount.subtract(discount));
Typical Mistakes
- Golden Hammer
// Using a pattern everywhere we know it // Creating Abstract Factory for a single object // Use only when truly needed // Simple if-else is better than unnecessary abstraction - Overengineering
- Too many levels of abstraction
- Hard to read and debug
- Stack trace with 50 levels
- Cargo Cult
- Blind copying without understanding
- Implementing Cloneable instead of a copy constructor
When NOT to use patterns
- Simple CRUD applications — direct code is more readable than abstractions
- Weekend prototypes — speed matters more than architecture
- When the pattern doesn’t solve your problem — don’t force the task to fit the pattern
Senior Level
Pattern as Architectural Contract
At a deeper level, a pattern defines:
- Separation of Concerns
- What objects are responsible for
- Clear boundaries between components
- Interaction Interfaces
- Minimized dependencies (Loose Coupling)
- Contracts between modules
- Extension Points
- Where the system can be extended without modifying existing code
- Open/Closed Principle in practice
Evolution of Patterns in Modern Java (8-21+)
Strategy / Command → Lambdas:
// Before: 10+ classes
// After: Function<T, R>, Consumer<T>, Predicate<T>
Template Method → Composition:
// Instead of inheritance — passing lambdas to constructor
public class Processor {
private final Function<Data, Result> transformer;
public Processor(Function<Data, Result> transformer) {
this.transformer = transformer;
}
}
Singleton / Prototype → DI Container:
// Spring manages the lifecycle
@Component // Singleton scope by default
public class UserService { }
@Scope("prototype")
public class OrderProcessor { }
Proxy → Spring AOP:
// @Transactional, @Cacheable — all are Proxy under the hood
// JDK Dynamic Proxy or CGLIB under the hood
@Transactional
public void processOrder() { }
Architectural Trade-offs
What we gain:
- Flexibility and extensibility
- Testability
- Clarity for those familiar with the pattern
- Loose coupling
What we lose:
- Increased number of classes
- Levels of indirection
- Debugging complexity
- Performance overhead
Megamorphic Calls and JIT
// Patterns affect JVM performance!
// Monomorphic call (1 implementation) → inlining
PaymentStrategy strategy = new CreditCard();
strategy.pay(); // JIT inlines the code → 0 overhead
// Bimorphic call (2 implementations) → still fast
// Megamorphic call (>2 implementations) → indirect call via vtable
// → Measurable CPU drop in hot path!
Optimization:
// For critical paths: enum instead of interface
public enum PaymentType {
CREDIT_CARD {
@Override public void pay() { /* code */ }
},
PAYPAL {
@Override public void pay() { /* code */ }
};
public abstract void pay();
}
// → JIT better optimizes enum switch
Patterns in Distributed Systems
Cloud-Native patterns:
Circuit Breaker → fault tolerance
Saga → distributed transactions
Sidecar → infrastructure alongside the service
CQRS → separation of reads and writes
Event Sourcing → event journal instead of state
Pattern Interconnections:
Abstract Factory (Creational)
→ returns Proxy (Structural)
→ which wraps business logic
→ managed via Strategy (Behavioral)
Chain of Responsibility (Behavioral)
→ assembled via Builder (Creational)
Facade (Structural)
→ simplifies interface to a subsystem
→ managed via Strategy (Behavioral)
Sufficiency Principle
The best pattern is the one that solves the task with minimal code:
// Overengineering
public interface MessageValidator { boolean validate(Message m); }
public class EmailValidator implements MessageValidator { ... }
public class PhoneValidator implements MessageValidator { ... }
public class ValidatorFactory { ... }
public class ValidationStrategy { ... }
// Sufficient
public boolean isValid(Message m) {
return m.getType() == EMAIL
? emailRegex.matcher(m.getContent()).matches()
: phoneRegex.matcher(m.getContent()).matches();
}
// Pattern needed ONLY if:
// - Extension is planned (new validation types)
// - Complex logic in each branch
// - Needs to be tested separately
Production Experience
Real scenario #1: Overengineering killed readability
- Project: 15 patterns in 10,000 lines of code
- Problem: new developer spends 2 weeks on onboarding
- Solution: simplified to 5 main patterns
- Result: 3-day onboarding, fewer bugs
Real scenario #2: Pattern misuse in Spring
- Manual Singleton instead of @Component
- Problem: impossible to mock in tests
- Solution: delegated to Spring IoC
- Result: testability + flexibility
Best Practices
- Know patterns, but don’t use them unnecessarily
- Modern Java simplifies many patterns (lambdas, Records)
- DI container replaces manual Singleton/Prototype/Factory
- Composition > Inheritance — more flexible and less fragile
- Simplicity — if if-else is enough, don’t overcomplicate
- Document — why you chose this pattern
- Verify — does the pattern violate SOLID
- Think about JIT — megamorphic calls affect CPU
Senior Summary
- Patterns are a tool for fighting complexity, not a goal
- Modern Java makes many patterns “invisible” (lambdas, Records)
- DI Container — your Singleton/Factory/Prototype manager
- Trade-offs: flexibility vs readability, extensibility vs complexity
- JIT optimizations: monomorphic > megamorphic calls
- Sufficiency principle: the best pattern = minimal code
- Distributed systems — new patterns (Saga, Circuit Breaker)
- Patterns should respond to a real need, not a theoretical one
Interview Cheat Sheet
Must know:
- Patterns are proven solutions to typical problems, saving 2-5 hours per task
- Three categories: Creational (creation), Structural (composition), Behavioral (interaction)
- GoF (Gang of Four) — Gamma, Helm, Johnson, Vlissides, authors of the 1994 book
- Modern Java (8-21) simplifies many patterns: lambdas replace Strategy, DI replaces Singleton
- Sufficiency principle: the best pattern = minimal code, if if-else is enough — don’t overcomplicate
- Overengineering kills readability: 15 patterns in 10K lines — that’s an anti-pattern
- Composition is preferred over inheritance — more flexible and less fragile
- Megamorphic calls (>2 implementations) affect JIT optimizations and CPU in hot paths
Common follow-up questions:
- When NOT to use patterns? — Simple CRUD, prototypes, pattern doesn’t solve the problem
- How does an idiom differ from a pattern? — Idiom is language-specific (try-with-resources), pattern is universal
- What cloud-native patterns do you know? — Circuit Breaker, Saga, CQRS, Event Sourcing, Sidecar
- What is a Golden Hammer? — Using a favorite pattern everywhere possible
Red flags (DO NOT say):
- “I use patterns everywhere I know them” — that’s overengineering
- “Singleton solves all problems” — Singleton is considered an anti-pattern in many cases
- “Patterns always improve performance” — often they add overhead
- “I don’t use patterns, I write simpler” — ignorance of basic architectural solutions
Related topics:
- [[02. What pattern categories exist]] — detailed classification
- [[16. What anti-patterns do you know]] — bad solutions that harm code
- [[07. Difference between Factory Method and Abstract Factory]] — creational patterns
- [[10. When to use Strategy]] — behavioral patterns in practice
- [[12. Advantage of Decorator over inheritance]] — composition vs inheritance