C#  

Abstract Factory Pattern for Notification Services in C# 14

Why Use Abstract Factory for Notifications?

Modern applications often need to send notifications via multiple channels—email, SMS, and push. And each of those channels might have multiple third-party providers depending on region, cost, or reliability. For example, you might send emails through SendGrid in production, Mailgun in dev, and Amazon SES for transactional messages. If your code directly instantiates a provider class (like new SendGridEmailService()), it becomes tightly coupled and brittle.

The Abstract Factory pattern lets you cleanly encapsulate the creation of related services, such as EmailService, SmsService, and PushNotificationService. It allows you to swap in new provider families (e.g., Twilio, Firebase, SNS) without changing any client logic. This design promotes clean separation of concerns, better testability, and full support for runtime switching via config, dependency injection, or user preference.

Real-World Structure

Let’s say your app needs to,

  • Send emails (confirmation, password reset)
  • Send SMS (2FA, alerts)
  • Send push notifications (mobile or web)

You want to support multiple notification providers.

  • SendGrid for email
  • Twilio for SMS
  • Firebase Cloud Messaging (FCM) for push notifications

Let’s design,

  • Abstract interfaces for each service
  • An abstract factory to create them
  • Concrete factory implementations for Twilio/SendGrid/FCM
  • Client code that doesn't care which backend it's using

Step 1. Define Service Interfaces.

Each service has its own responsibility. We start with abstract interfaces.

public interface IEmailService
{
    void SendEmail(string to, string subject, string body);
}

public interface ISmsService
{
    void SendSms(string phoneNumber, string message);
}

public interface IPushNotificationService
{
    void SendPush(string deviceToken, string title, string message);
}

Each of these interfaces defines a simple contract. Whether you're using Twilio, SendGrid, Firebase, or mock implementations, they all follow these contracts so the rest of the app doesn't need to change.

Step 2. Define the Abstract Factory.

Now, we define the abstract factory interface that creates the related family of notification services.

public interface INotificationFactory
{
    IEmailService CreateEmailService();
    ISmsService CreateSmsService();
    IPushNotificationService CreatePushService();
}

This factory is the core abstraction. It defines how to get the right implementation of each service. Depending on which factory you inject—TwilioFactory, FirebaseFactory, etc.—you'll get the corresponding set of services.

Step 3. SendGrid + Twilio + Firebase Implementation.

Let’s implement a full provider set. We’ll simulate the behavior with Console.WriteLine, but these would wrap real SDKs in a production app.

SendGrid Email Service

public class SendGridEmailService : IEmailService
{
    public void SendEmail(string to, string subject, string body)
    {
        Console.WriteLine($"[SendGrid] Sending email to {to}: {subject} - {body}");
    }
}

Twilio SMS Service

public class TwilioSmsService : ISmsService
{
    public void SendSms(string phoneNumber, string message)
    {
        Console.WriteLine($"[Twilio] Sending SMS to {phoneNumber}: {message}");
    }
}

Firebase Push Notification

public class FirebasePushService : IPushNotificationService
{
    public void SendPush(string deviceToken, string title, string message)
    {
        Console.WriteLine($"[Firebase] Push to {deviceToken}: {title} - {message}");
    }
}

These classes are isolated, easy to test individually, and swappable. Each can be extended with real API logic and credentials securely loaded from configuration.

Step 4. The Provider Factory.

Now let’s create a factory that returns the above services.

public class CloudNotificationFactory : INotificationFactory
{
    public IEmailService CreateEmailService() => new SendGridEmailService();
    public ISmsService CreateSmsService() => new TwilioSmsService();
    public IPushNotificationService CreatePushService() => new FirebasePushService();
}

This factory encapsulates the full "cloud-based" provider set. You could later create TestNotificationFactory, EnterpriseNotificationFactory, or even NullNotificationFactory for silent mode.

Step 5. Client Code.

Now we create a service that uses the notification factory.

public class NotificationManager
{
    private readonly IEmailService _email;
    private readonly ISmsService _sms;
    private readonly IPushNotificationService _push;

    public NotificationManager(INotificationFactory factory)
    {
        _email = factory.CreateEmailService();
        _sms = factory.CreateSmsService();
        _push = factory.CreatePushService();
    }

    public void SendUserNotifications()
    {
        _email.SendEmail("[email protected]", "Welcome!", "Thanks for signing up!");
        _sms.SendSms("+15551234567", "Your 2FA code is 123456");
        _push.SendPush("devicetoken123", "New Alert", "Check out the latest update.");
    }
}

This manager handles notification logic. It can be reused across environments without ever knowing what service is used behind the scenes.

Program Entry Point

class Program
{
    static void Main()
    {
        // Select provider factory
        INotificationFactory factory = new CloudNotificationFactory();

        var notifier = new NotificationManager(factory);
        notifier.SendUserNotifications();
    }
}

This setup means you can swap notification ecosystems by changing one line. In real apps, this could be injected via ASP.NET Core’s dependency injection system.

Real-World Benefits

The Abstract Factory approach here gives you,

  • Pluggability: Switch providers (e.g., AWS vs Azure) without rewriting logic
  • Testability: Mock services for unit tests (e.g., TestNotificationFactory)
  • Extensibility: Add Slack or Discord in future without changing the NotificationManager
  • Clean architecture: No service knows about concrete implementations

This also scales really well when you introduce priority-based delivery, multi-region fallback, or tiered plans (e.g., premium users get SMS + push).

Bonus: Test Notification Factory

public class TestNotificationFactory : INotificationFactory
{
    public IEmailService CreateEmailService() => new FakeEmailService();
    public ISmsService CreateSmsService() => new FakeSmsService();
    public IPushNotificationService CreatePushService() => new FakePushService();
}

public class FakeEmailService : IEmailService
{
    public void SendEmail(string to, string subject, string body) =>
        Console.WriteLine($"[TEST] Email: {to}, {subject}");
}
public class FakeSmsService : ISmsService
{
    public void SendSms(string number, string message) =>
        Console.WriteLine($"[TEST] SMS: {number}, {message}");
}
public class FakePushService : IPushNotificationService
{
    public void SendPush(string token, string title, string message) =>
        Console.WriteLine($"[TEST] Push: {token}, {title}");
}

Use this in dev or CI pipelines to validate notification logic without sending actual messages.

Summary

The Abstract Factory Pattern is a smart, scalable solution for multi-channel notification systems. It enables your application to stay agnostic to provider details, promotes clean code and loose coupling, and makes testing and scaling straightforward.

Feature Benefit
Multiple providers Easily swap Twilio, SendGrid, Firebase, etc.
Test-friendly Inject mock factories with zero impact on client logic
Open/Closed Compliant Add new services or providers with minimal changes