Design pattern | Adapter
Learn how the Adapter Pattern enables seamless integration between incompatible interfaces. Explore both class-based and functional implementations for scalable and maintainable solutions.

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 😊
Discussion