← Back to scenarios
ScenarioCatalog DB

Coffee customization

Add extras such as milk, mocha, and whip to a base drink while composing behavior and cost.

Problem

An ordering system must let customers combine optional drink additions and calculate the resulting behavior and price without defining a separate concrete type for every possible order combination.

Related patterns

Implementations

Decorator

TypeScriptApplication

Decorator in TypeScript

Adds logging and retry behavior to a notification sender by composing wrappers that preserve the same sending interface.

TypeScript
interface NotificationSender {
  send(recipient: string, message: string): Promise<void>;
}

class EmailSender implements NotificationSender {
  async send(recipient: string, message: string) {
    console.log(`Sending email to ${recipient}: ${message}`);
  }
}

abstract class NotificationSenderDecorator
  implements NotificationSender
{
  constructor(
    protected readonly sender: NotificationSender,
  ) {}

  send(recipient: string, message: string) {
    return this.sender.send(recipient, message);
  }
}

class LoggingSender extends NotificationSenderDecorator {
  async send(recipient: string, message: string) {
    console.log(`Starting notification for ${recipient}`);

    await super.send(recipient, message);

    console.log(`Completed notification for ${recipient}`);
  }
}

class RetryingSender extends NotificationSenderDecorator {
  async send(recipient: string, message: string) {
    try {
      await super.send(recipient, message);
    } catch (error) {
      console.warn("First attempt failed; retrying once.");
      await super.send(recipient, message);
    }
  }
}

const sender = new LoggingSender(
  new RetryingSender(new EmailSender()),
);

void sender.send("team@example.com", "Deployment completed.");

Associated technologies