Structural patterns organize how objects and classes compose to form larger structures. These three solve composition puzzles without inheritance.
Decorator Pattern
What it does: Dynamically add responsibilities to an object. Wrap an object in a decorator that adds behavior without modifying the original.
Where it appears: I/O streams (BufferedInputStream wraps InputStream), middleware, logging/timing wrappers.
Example: Coffee Shop with Toppings
interface Coffee {
double getCost();
String getDescription();
}
class SimpleCoffee implements Coffee {
@Override
public double getCost() {
return 2.00;
}
@Override
public String getDescription() {
return "Simple Coffee";
}
}
// Decorators add toppings
abstract class CoffeeDecorator implements Coffee {
protected Coffee coffee;
public CoffeeDecorator(Coffee coffee) {
this.coffee = coffee;
}
}
class MilkDecorator extends CoffeeDecorator {
public MilkDecorator(Coffee coffee) {
super(coffee);
}
@Override
public double getCost() {
return coffee.getCost() + 0.50;
}
@Override
public String getDescription() {
return coffee.getDescription() + ", Milk";
}
}
class WhippedCreamDecorator extends CoffeeDecorator {
public WhippedCreamDecorator(Coffee coffee) {
super(coffee);
}
@Override
public double getCost() {
return coffee.getCost() + 0.75;
}
@Override
public String getDescription() {
return coffee.getDescription() + ", Whipped Cream";
}
}
// Usage: build complex objects step-by-step
Coffee coffee = new SimpleCoffee();
coffee = new MilkDecorator(coffee);
coffee = new WhippedCreamDecorator(coffee);
System.out.println(coffee.getDescription()); // "Simple Coffee, Milk, Whipped Cream"
System.out.println(coffee.getCost()); // 3.25
Why it beats inheritance: Inheritance would require SimpleCoffeeWithMilk, SimpleCoffeeWithMilkAndWhippedCream, etc.—a combinatorial explosion. Decorators compose behaviors dynamically.
Adapter Pattern
What it does: Convert an interface to another interface that a client expects. Make incompatible things work together.
Where it appears: Integrating legacy APIs, third-party libraries, converting data formats.
Example: Payment Gateway Adapter
// Our internal interface
interface PaymentProcessor {
boolean charge(double amount, String cardToken);
}
// Third-party Stripe SDK (incompatible interface)
class StripeClient {
public StripeResponse chargeCard(StripeRequest req) {
// Stripe's way: pass a StripeRequest, get StripeResponse
}
}
// Adapter: makes Stripe implement our PaymentProcessor interface
class StripeAdapter implements PaymentProcessor {
private final StripeClient stripeClient;
public StripeAdapter(StripeClient stripeClient) {
this.stripeClient = stripeClient;
}
@Override
public boolean charge(double amount, String cardToken) {
StripeRequest req = new StripeRequest();
req.setAmount((int)(amount * 100)); // Stripe uses cents
req.setToken(cardToken);
StripeResponse res = stripeClient.chargeCard(req);
return res.isSuccess();
}
}
// Client code: works with our interface, not Stripe's
class CheckoutService {
private final PaymentProcessor processor;
public CheckoutService(PaymentProcessor processor) {
this.processor = processor;
}
public void checkout(double amount, String cardToken) {
if (processor.charge(amount, cardToken)) {
System.out.println("Payment successful");
} else {
System.out.println("Payment failed");
}
}
}
// Usage: swap payment gateways by swapping adapters
PaymentProcessor stripe = new StripeAdapter(new StripeClient());
PaymentProcessor paypal = new PayPalAdapter(new PayPalClient());
CheckoutService checkout = new CheckoutService(stripe); // or paypal
Facade Pattern
What it does: Provide a simplified, unified interface to a complex subsystem.
Where it appears: Libraries that wrap multiple components, system initialization, complex workflows.
Example: Home Automation Facade
// Complex subsystem: many components
class Lights {
public void on() { System.out.println("Lights on"); }
public void off() { System.out.println("Lights off"); }
}
class Thermostat {
public void setTemperature(int celsius) { System.out.println("Temperature set to " + celsius); }
}
class SecuritySystem {
public void arm() { System.out.println("Security armed"); }
public void disarm() { System.out.println("Security disarmed"); }
}
class AudioSystem {
public void playMusic(String song) { System.out.println("Playing " + song); }
public void stop() { System.out.println("Music stopped"); }
}
// Facade: simple interface for complex workflows
class HomeAutomationFacade {
private final Lights lights;
private final Thermostat thermostat;
private final SecuritySystem security;
private final AudioSystem audio;
public HomeAutomationFacade(Lights lights, Thermostat thermostat, SecuritySystem security, AudioSystem audio) {
this.lights = lights;
this.thermostat = thermostat;
this.security = security;
this.audio = audio;
}
public void goodMorning() {
lights.on();
thermostat.setTemperature(22);
security.disarm();
audio.playMusic("Morning News");
}
public void leavingHome() {
lights.off();
thermostat.setTemperature(18);
security.arm();
audio.stop();
}
public void movieMode() {
lights.off();
thermostat.setTemperature(21);
audio.playMusic("Movie Soundtrack");
}
}
// Client: simple, clean
HomeAutomationFacade home = new HomeAutomationFacade(...);
home.goodMorning(); // one call, multiple subsystems coordinated
Choosing Under Pressure
Interviewers like when you can quickly pick the right pattern. Here's a cheat sheet:
Requirement → Pattern
| Requirement | Pattern | Why | |---|---|---| | Add behavior without modifying existing class | Decorator | Composition, flexible, combines behaviors | | Integrate incompatible third-party API | Adapter | Converts interfaces, isolates third-party coupling | | Simplify a complex system for callers | Facade | Single entry point, hides complexity | | One class, many representations (is-a) | Inheritance | Direct, immutable structure | | One object, many optional add-ons | Decorator | Dynamic, composable | | Existing class in wrong interface | Adapter | Bridge the gap without rewriting | | Subsystem with multiple steps | Facade | Workflow orchestration |
In Practice
Interviewer: "Design a notification system. Users can get email, SMS, or push. Each can have a retry wrapper. Each can be rate-limited."
Your thinking:
- Multiple notification channels → Strategy or interface (NotificationChannel).
- Add retry/rate-limit wrapping → Decorator.
- Orchestrate complex workflows → Facade.
Your code:
interface Notification {
void send();
}
class EmailNotification implements Notification {
@Override
public void send() { /* send email */ }
}
// Decorators add cross-cutting concerns
class RetryDecorator implements Notification {
private final Notification inner;
private final int maxRetries;
public RetryDecorator(Notification inner, int maxRetries) {
this.inner = inner;
this.maxRetries = maxRetries;
}
@Override
public void send() {
for (int i = 0; i < maxRetries; i++) {
try {
inner.send();
return;
} catch (Exception e) {
if (i == maxRetries - 1) throw e;
}
}
}
}
class RateLimitDecorator implements Notification {
private final Notification inner;
private final RateLimiter limiter;
public RateLimitDecorator(Notification inner, RateLimiter limiter) {
this.inner = inner;
this.limiter = limiter;
}
@Override
public void send() {
limiter.checkAllowed();
inner.send();
}
}
// Facade: tie it all together
class NotificationFacade {
public Notification buildEmailWithRetryAndRateLimit() {
Notification email = new EmailNotification();
email = new RetryDecorator(email, 3);
email = new RateLimitDecorator(email, new RateLimiter(10, 60000));
return email;
}
}
In the interview
-
Listen for the friction. "Third-party SDK has a different interface" → Adapter. "Adding behavior without modifying" → Decorator. "Complex subsystem with many steps" → Facade.
-
Start simple, add patterns as needed. Don't reach for Decorator if there's only one add-on. Wait for a second requirement.
-
Name the pattern and the problem it solves. "Notification and Email have different interfaces. I'll use Adapter to convert Email to implement Notification."
-
Show composition. Decorators are great for talking points: "I wrap EmailNotification in RetryDecorator, then in RateLimitDecorator. Each adds a layer of behavior."