Why Design Patterns Matter
Design patterns are reusable solutions to recurring design problems. They are not copy-paste code; they are vocabulary for discussing tradeoffs in object-oriented design.
High-Frequency Patterns
| Pattern | Intent | Java Interview Example |
|---|---|---|
| Singleton | Ensure one shared instance | Configuration registry, logger |
| Adapter | Convert one interface to another | Wrap a legacy payment client behind a new interface |
| Builder | Construct complex objects step by step | Immutable request/DTO object with many optional fields |
| Decorator | Add behavior without changing the original class | Logging, caching, or validation around a service |
Pros and Cons
Patterns improve readability when the team recognizes the pattern. They also reduce duplicated design decisions. The risk is overengineering: using a pattern where a simple class would be clearer.
Interview Framing
Say what problem the pattern solves, show a small Java example, then explain the tradeoff. For example, Builder helps avoid huge constructors, but it adds extra code.
Java Code Examples
Singleton
public final class AppConfig {
private static final AppConfig INSTANCE = new AppConfig();
private AppConfig() {}
public static AppConfig getInstance() {
return INSTANCE;
}
}
Adapter
interface PaymentGateway {
void pay(int amount);
}
class LegacyPaymentClient {
void makePayment(int amountInRupees) {
System.out.println("Paid " + amountInRupees);
}
}
class LegacyPaymentAdapter implements PaymentGateway {
private final LegacyPaymentClient client;
LegacyPaymentAdapter(LegacyPaymentClient client) {
this.client = client;
}
public void pay(int amount) {
client.makePayment(amount);
}
}
Builder
class UserProfile {
private final String name;
private final String email;
private final boolean newsletterEnabled;
private UserProfile(Builder builder) {
this.name = builder.name;
this.email = builder.email;
this.newsletterEnabled = builder.newsletterEnabled;
}
static class Builder {
private String name;
private String email;
private boolean newsletterEnabled;
Builder name(String name) {
this.name = name;
return this;
}
Builder email(String email) {
this.email = email;
return this;
}
Builder newsletterEnabled(boolean enabled) {
this.newsletterEnabled = enabled;
return this;
}
UserProfile build() {
return new UserProfile(this);
}
}
}
Decorator
interface OrderService {
void placeOrder(String orderId);
}
class BasicOrderService implements OrderService {
public void placeOrder(String orderId) {
System.out.println("Order placed: " + orderId);
}
}
class LoggingOrderService implements OrderService {
private final OrderService delegate;
LoggingOrderService(OrderService delegate) {
this.delegate = delegate;
}
public void placeOrder(String orderId) {
System.out.println("Before order: " + orderId);
delegate.placeOrder(orderId);
System.out.println("After order: " + orderId);
}
}
Interview Scenario Practice
Scenario 1: Payment SDK Changed
Scenario: A payment provider changed its SDK method names, but your application wants to keep using the same PaymentGateway interface.
Strong answer: Use the Adapter pattern. Keep the application dependent on PaymentGateway, then write an adapter that translates your interface calls into the new SDK calls.
Why it works: The rest of the codebase stays stable while the integration detail is isolated in one class.
Common mistake: Editing every service class to call the new SDK directly. That spreads vendor-specific code everywhere.
Scenario 2: Too Many DTO Constructor Parameters
Scenario: A request DTO has many optional fields and constructors are becoming unreadable.
Strong answer: Use Builder when object creation has many optional values or when readability matters more than a short constructor.
Why it works: Named builder methods make object creation self-documenting and reduce constructor-order mistakes.
Common mistake: Creating multiple overloaded constructors with similar parameter types. That makes bugs easy to miss.
Scenario 3: Add Logging Without Editing Service Code
Scenario: You need to add logging or metrics around an existing service without changing the service class.
Strong answer: Use Decorator. Wrap the original service in another implementation of the same interface and add logging before or after delegating.
Why it works: It follows composition and keeps the original class focused on business behavior.
Common mistake: Putting logging, validation, caching, and business logic into one large service class.