The Adapter Design Pattern is a structural pattern that allows incompatible interfaces to work together by introducing an intermediary—the adapter—that translates one interface into another. In this post, we will explore how to apply this pattern to integrate different email service providers, such as SendGrid and Mailgun, into a TypeScript application.

Use Case: Integrating Multiple Email Providers

Suppose our application needs to send emails, and we want the flexibility to switch between providers (e.g., SendGrid and Mailgun) without modifying the core logic. The Adapter Pattern allows us to do this seamlessly.

Adapter Pattern Variants

There are two main ways to implement the Adapter Pattern:

  • Object Adapter (Composition): The adapter references an instance of the class it wraps and delegates calls to it.
  • Class Adapter (Inheritance): The adapter inherits from both the interface the client expects and the class that needs adapting (in languages that support multiple inheritance).

OOP Implementation (Object Adapter)

1. Define a Common Interface

The first step is defining an interface that all email providers will implement.

interface EmailProvider {
  sendEmail(to: string, subject: string, body: string): Promise<void>;
}

2. Implement Adapters for Each Provider

Adapter for SendGrid

class SendGridAdapter implements EmailProvider {
  private sendGridClient: MailService;

  constructor(sendGridClient: MailService) {
    this.sendGridClient = sendGridClient;
  }

  async sendEmail(to: string, subject: string, body: string): Promise<void> {
    const message = {
      to,
      from: '[email protected]',
      subject,
      text: body,
    };
    await this.sendGridClient.send(message);
  }
}

Adapter for Mailgun

class MailgunAdapter implements EmailProvider {
  private mailgunClient: Client;

  constructor(mailgunClient: Client) {
    this.mailgunClient = mailgunClient;
  }

  async sendEmail(to: string, subject: string, body: string): Promise<void> {
    const data = {
      from: '[email protected]',
      to,
      subject,
      text: body,
    };
    await this.mailgunClient.messages.create('YOUR_DOMAIN_NAME', data);
  }
}

3. Using the Adapters in Our Application

async function sendNotification(
  provider: EmailProvider,
  recipient: string,
  subject: string,
  content: string
): Promise<void> {
  await provider.sendEmail(recipient, subject, content);
}

const sendGridClient = {} as MailService;
const mailgunClient = {} as Client;

const sendGridAdapter = new SendGridAdapter(sendGridClient);
const mailgunAdapter = new MailgunAdapter(mailgunClient);

sendNotification(sendGridAdapter, '[email protected]', 'Subject', 'Email content');
sendNotification(mailgunAdapter, '[email protected]', 'Subject', 'Email content');

OOP Implementation (Class Adapter)

Although TypeScript doesn't support multiple inheritance, you can simulate a class adapter by extending a base class and delegating calls internally. Here's an implementation based on the same email provider use case, using inheritance to provide a consistent interface:

1. Abstract Base Class (Target Interface)

abstract class EmailProvider {
  abstract sendEmail(to: string, subject: string, body: string): Promise<void>;
}

2. Adaptee: SendGridClient

class SendGridClient {
  send(to: string, payload: { subject: string; text: string }): Promise<void> {
    console.log(`[SendGrid] Sending email to ${to}: ${payload.subject}`);
    return Promise.resolve();
  }
}

3. Adaptee: MailgunClient

class MailgunClient {
  sendMessage(domain: string, data: { to: string; subject: string; text: string }): Promise<void> {
    console.log(`[Mailgun] Sending email to ${data.to}: ${data.subject}`);
    return Promise.resolve();
  }
}

4. Adapter for SendGridClient

class SendGridAdapter extends EmailProvider {
  constructor(private sendGridClient: SendGridClient) {
    super();
  }

  async sendEmail(to: string, subject: string, body: string): Promise<void> {
    await this.sendGridClient.send(to, { subject, text: body });
  }
}

5. Adapter for MailgunClient

class MailgunAdapter extends EmailProvider {
  constructor(private mailgunClient: MailgunClient, private domain: string) {
    super();
  }

  async sendEmail(to: string, subject: string, body: string): Promise<void> {
    await this.mailgunClient.sendMessage(this.domain, {
      to,
      subject,
      text: body,
    });
  }
}

6. Using the Class Adapters

async function sendNotification(
  provider: EmailProvider,
  recipient: string,
  subject: string,
  content: string
): Promise<void> {
  await provider.sendEmail(recipient, subject, content);
}

const sendGrid = new SendGridClient();
const mailgun = new MailgunClient();

const sendGridAdapter = new SendGridAdapter(sendGrid);
const mailgunAdapter = new MailgunAdapter(mailgun, 'your-domain.com');

sendNotification(sendGridAdapter, '[email protected]', 'Subject', 'Email content');
sendNotification(mailgunAdapter, '[email protected]', 'Subject', 'Email content');

Implementation using Functional Programming (Object Adapter)

1. Define the Common Interface

type SendEmail = (to: string, subject: string, body: string) => Promise<void>;

2. Create Functional Adapters

Adapter for SendGrid

const sendGridAdapter = (sendGridClient: MailService): SendEmail => {
  return async (to, subject, body) => {
    const message = {
      to,
      from: '[email protected]',
      subject,
      text: body,
    };
    await sendGridClient.send(message);
  };
};

Adapter for Mailgun

const mailgunAdapter = (mailgunClient: Client): SendEmail => {
  return async (to, subject, body) => {
    const data = {
      from: '[email protected]',
      to,
      subject,
      text: body,
    };
    await mailgunClient.messages.create('YOUR_DOMAIN_NAME', data);
  };
};

3. Using Functional Adapters

async function sendNotification(
  sendEmail: SendEmail,
  recipient: string,
  subject: string,
  content: string
): Promise<void> {
  await sendEmail(recipient, subject, content);
}

const sendGridClient = {} as MailService;
const mailgunClient = {} as Client;

const sendEmailWithSendGrid = sendGridAdapter(sendGridClient);
const sendEmailWithMailgun = mailgunAdapter(mailgunClient);

sendNotification(sendEmailWithSendGrid, '[email protected]', 'Subject', 'Email content');
sendNotification(sendEmailWithMailgun, '[email protected]', 'Subject', 'Email content');
  • Flexibility: Easily switch between different providers without modifying the core application logic.
  • Maintainability: Encapsulate vendor-specific code within adapters, keeping the main codebase clean.
  • Scalability: Add new providers by simply creating new adapters that conform to the common interface.

Conclusion

The Adapter Pattern is a powerful solution for integrating different third-party services into a unified system. Whether using classes or functions—or extending base classes—the goal is the same: decoupling the core logic from external dependencies.

Thanks for reading me 😊