SwadhyAI Notes

All Low-Level Design notes

Structural · same type, add behavior

Decorator

A is a structural pattern that adds behavior by wrapping an object that already implements the same interface. The wrapper does extra work, then forwards the call. Callers still use the original type.

A concrete example: you send order emails, then product wants a log line, then retry on timeout, then an “[URGENT]” prefix. EmailLoggingRetryUrgentNotifier is not a type. Wrap a Notifier with another Notifier. them. The caller still calls send().

Nested rings — one Notifier all the way down

RetryingNotifier

also retry

LoggingNotifier

also log

UrgentPrefixNotifier

also prefix

EmailNotifier

the real send()

Caller → send() hits the outer ring first. The inner object was passed in, already alive.

Core question

Can I add behavior by wrapping the same interface the caller already uses?

Keep the . Take the . Do extra work, then delegate. Stack on purpose. That is without a subclass for every combination.

The combo explosion you refuse to subclass

Inheritance combos

  • EmailNotifier
  • LoggingEmailNotifier
  • RetryEmailNotifier
  • LoggingRetryEmailNotifier
  • UrgentLoggingRetryEmailNotifier

2^n classes. A new concern doubles the list.

Three small wrappers

new RetryingNotifier(
  new LoggingNotifier(
    new UrgentPrefixNotifier(email)),
  3)

Order matters: retry-outside-logging logs every attempt; logging-outside-retry logs once per successful send.

Java sketch: logging and retry wrap the same Notifier

java
interface Notifier {
  void send(String to, String body);
}

class EmailNotifier implements Notifier {
  public void send(String to, String body) { /* SMTP */ }
}

class LoggingNotifier implements Notifier {
  private final Notifier inner;
  LoggingNotifier(Notifier inner) { this.inner = inner; }
  public void send(String to, String body) {
    System.out.println("sending to " + to);
    inner.send(to, body);
  }
}

class RetryingNotifier implements Notifier {
  private final Notifier inner;
  private final int maxAttempts; // bound attempts
  RetryingNotifier(Notifier inner, int maxAttempts) {
    this.inner = inner;
    this.maxAttempts = maxAttempts;
  }

  public void send(String to, String body) {
    // Prefer an idempotent send (or idempotency key) so a retry cannot double-deliver.
    for (int attempt = 1; attempt <= maxAttempts; attempt++) {
      try {
        inner.send(to, body);
        return;
      } catch (RuntimeException e) {
        if (!isRetryable(e) || attempt == maxAttempts) throw e;
        sleepBackoff(attempt); // optional: exponential backoff + jitter
      }
    }
  }

  private static boolean isRetryable(RuntimeException e) {
    return e instanceof TransientFailure; // timeouts, 503 — not validation or auth
  }

  private static void sleepBackoff(int attempt) {
    try {
      Thread.sleep(Math.min(1000L, 50L << (attempt - 1)));
    } catch (InterruptedException ie) {
      Thread.currentThread().interrupt();
      throw new RuntimeException(ie);
    }
  }
}

// Notifier n = new RetryingNotifier(new LoggingNotifier(new EmailNotifier()), 3);

Java I/O is the living example

new BufferedInputStream(new FileInputStream(file)). Same InputStream, extra buffering. Compression, encryption, and counting bytes are more layers. Interview twins: logging, retry, caching a get, compressing a payload. See Notification for the product-shaped version.

Decorator is not Adapter, Facade, or Proxy

The class diagram of Decorator and Proxy is almost the same box wrapping a box. Intent is what you are graded on. Adapter and Facade are not even the same diagram.

Decorator — “also do X”

  • Same interface as the inner object.
  • Inner is constructed first, then handed in.
  • Adds a feature the client is glad to stack: log, retry, compress, prefix.
  • Nesting three wrappers is normal.
  • Does not delay creation, check ACL, or hop the network as its reason to exist.

Proxy — “only if / not yet / over there”

  • Same interface — that is where the confusion starts.
  • Often owns creation: DiskImage is born on first display().
  • Controls access, lifetime, or location — not a new product feature.
  • You rarely stack three proxies for logging + retry + prefix.
  • A logging wrapper named Proxy is a misnamed decorator.

Adapter — “wrong shape”

StripeClient cannot be Payable without mapping. The wrapper is a different type than the wrappee. If your decorator is no longer a Notifier, you did not extend behavior — you translated types.

Facade — “too many collaborators”

Hides many classes behind one simpler method. A decorator hides nothing: the caller still sees Notifier, and there is still one inner Notifier, not four departments.

How to implement it (four steps)

  1. 01

    Keep the interface

    The wrapper is a Notifier — not a type the caller must learn.

  2. 02

    Take the inner object

    Constructor injection. Tests wrap a fake. Production wraps the real emailer.

  3. 03

    Do extra work, then delegate

    Log, then inner.send. Retry only eligible failures, bound attempts, prefer idempotent sends — then delegate.

  4. 04

    Stack on purpose

    Order matters. Say retry-outside-logging out loud.

Common mistakes

A decorator that changes the interface

If the wrapper is no longer a Notifier, you translated types. That is Adapter. Decorators are substitutable for the thing they wrap.

Calling lazy load a Decorator

“I wrap Image so it loads from disk on first display” is a virtual Proxy. The intent is control of creation, not extra behavior. Logging that same Image is Decorator.

One wrapper that logs, retries, and prefixes

You rebuilt EmailLoggingRetryUrgentNotifier. Split the rings. Each class has one extra job.

Retrying every RuntimeException

Validation and auth failures will not heal. Retry only transient failures, bound maxAttempts, prefer an idempotent send (or key), and optionally back off. Blind retry doubles deliveries.

Swallowing the real send

A decorator that logs and returns without calling inner.send is not adding behavior. It is replacing the object. Forward unless the extra work is a short-circuit the client asked for (cache hit on a get is a rare honest exception).

How to explain this in an interview

“Retry and logging are extra behavior on the same Notifier, so I will decorate. The emailer already exists and is passed in. This is not a proxy — we are not delaying construction. This is not an adapter — the type is already Notifier.”

Draw three rings around EmailNotifier. Label which ring runs first on send().

Stripe must become Payable, then you want retry. Where does Adapter sit, and where does this page sit?

Takeaway

Decorator means “also do X” on the same type, with a live inner object, stacked on purpose. Wrong shape is Adapter. Too many collaborators is Facade. Not yet / only if / over there is Proxy.