Early Access: 87 spots left.

Claim
Low Level DesignDecoratorDecorator: Wrap a Notifier with Logging and Retries

Course content

Decorator: Wrap a Notifier with Logging and Retries

Medium·Tagsdesign-patternsdecoratorstructuralcomposition

Problem Statement

The application uses a `Notifier` interface for sending messages: ```java interface Notifier { void send(String to, String message); // may throw RuntimeException on failure } ``` `EmailNotifier implements Notifier` is the concrete leaf and is already implemented. Two cross-cutting concerns — logging and retries — need to apply to any Notifier (email, future Slack/SMS channels, mocks for testing). Implement them as separate decorator classes so they compose at wiring time: ```java class LoggingNotifier implements Notifier { public LoggingNotifier(Notifier wrapped); public void send(String to, String message); // 1. Print: "sending to <to>: <message>\n" to stdout (exact format). // 2. Delegate to the wrapped notifier. } class RetryingNotifier implements Notifier { public RetryingNotifier(Notifier wrapped, int maxAttempts); public void send(String to, String message); // Try wrapped.send up to maxAttempts times. If the call succeeds, return // immediately. If it throws RuntimeException, catch it and try again up // to (maxAttempts - 1) more times. If all attempts fail, re-throw the // last exception. } ``` With both decorators in place, the application can wire any chain at runtime: ```java Notifier robust = new RetryingNotifier(new LoggingNotifier(new EmailNotifier()), 3); robust.send("alice@x.com", "hello"); ``` The validator runs three checks: 1. logging_decorator — `LoggingNotifier` prints `sending to <to>: <message>` (with a trailing newline) BEFORE delegating, then calls the wrapped notifier exactly once with the same arguments. 2. retry_decorator — `RetryingNotifier` retries on `RuntimeException` and stops as soon as one attempt succeeds; if every attempt fails, it re-throws the last exception. Both branches are tested: a flaky notifier that fails twice then succeeds completes after 3 attempts; a notifier that always fails causes `send` to throw after exactly `maxAttempts` calls. 3. composition_and_stacking — both decorators implement `Notifier`, both accept a `Notifier` (and `RetryingNotifier` an `int`) via their constructor, and stacking `RetryingNotifier(LoggingNotifier(recording))` delivers the message to the inner recording notifier. The stub decorators throw `UnsupportedOperationException` from every method, so they fail every check until the methods are implemented.

Examples

Example 1
Input
var r = new Recording(); new LoggingNotifier(r).send("alice@x.com", "hi")
Output
"stdout: \"sending to alice@x.com: hi\\n\"; r received one send(\"alice@x.com\", \"hi\")"
Why
The decorator logs first, then delegates. Both effects happen on every send.
Example 2
Input
var flaky = new Flaky(failsBefore=2); new RetryingNotifier(flaky, 3).send("a", "m")
Output
"no exception; flaky was called 3 times"
Why
Two failures, third attempt succeeds. The retry decorator stops as soon as one attempt completes without throwing.
Example 3
Input
var always = new AlwaysFail(); new RetryingNotifier(always, 3).send("a", "m")
Output
"throws RuntimeException; always was called 3 times"
Why
All attempts failed. The decorator re-throws the last exception so the caller knows the operation gave up.

Constraints

  • LoggingNotifier and RetryingNotifier must implement the Notifier interface.
  • LoggingNotifier accepts a Notifier in its constructor and holds it as a field (composition).
  • RetryingNotifier accepts a Notifier and an int maxAttempts in its constructor.
  • LoggingNotifier prints exactly: "sending to <to>: <message>\n" to stdout before delegating.
  • RetryingNotifier catches RuntimeException and retries; on success it returns; on exhaustion it re-throws the last RuntimeException.
  • Neither decorator extends EmailNotifier — they wrap by composition, not inheritance.