SDE Path

Patterns III: Decorator, Adapter, Facade + Choosing Under Pressure

13 min read

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:

  1. Multiple notification channels → Strategy or interface (NotificationChannel).
  2. Add retry/rate-limit wrapping → Decorator.
  3. 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

  1. Listen for the friction. "Third-party SDK has a different interface" → Adapter. "Adding behavior without modifying" → Decorator. "Complex subsystem with many steps" → Facade.

  2. Start simple, add patterns as needed. Don't reach for Decorator if there's only one add-on. Wait for a second requirement.

  3. Name the pattern and the problem it solves. "Notification and Email have different interfaces. I'll use Adapter to convert Email to implement Notification."

  4. Show composition. Decorators are great for talking points: "I wrap EmailNotification in RetryDecorator, then in RateLimitDecorator. Each adds a layer of behavior."

Check yourself

5 questions — graded when you submit.

Question 1

Why use the Decorator pattern instead of inheritance when adding toppings (milk, cream) to a coffee?

Question 2

In the payment gateway example, why wrap StripeClient in a StripeAdapter instead of having CheckoutService use StripeClient directly?

Question 3

In the HomeAutomationFacade, the goodMorning() method calls multiple subsystems. Why is this a Facade and not just a plain method?

Question 4

You're adding retry logic to a notification. Should you use Decorator or Adapter?

Question 5

Which pattern is best for converting a legacy API (incompatible interface) to work with modern code?

Finished reading?

Sign in to save your progress across lessons.