Search code examples
.netasp.net-coresolid-principlesfallback

Email fallback following SOLID principles


I'd like to implement a microservice to send emails using a fallback client, so in case of failure in the first client (SendGrid) I'll call the second client (MailJet), the code below shows the idea.

The questions is: Is there a way to improve the Main function using some .net core feature instead of initialize new objects? The point is that I'd like to follow SOLID principles avoiding dependencies and tight couplings, so if I need a new EmailClient tomorrow it should be easy to implement without break SOLID principles.

P.S. Any improvement is welcome.

using System;
using System.Collections.Generic;
                    
public class Program
{
    public static void Main()
    {       
        List<IEmailClient> clients = new List<IEmailClient>();
        clients.Add(new SendGrid());
        clients.Add(new MailJet());
        
        var emailService = new EmailService(clients);
        emailService.sendEmail();
    }   
}

public class EmailService
{
    protected List<IEmailClient> clients;
    
    public EmailService(List<IEmailClient> clients)
    {
        this.clients = clients;
    }
    
    public void sendEmail()
    {
        foreach (IEmailClient client in this.clients)
        {
            if (client.send()) {
                break;
            }
        }
    }
}

public interface IEmailClient
{
    bool send();
}

public class SendGrid: IEmailClient
{
    public bool send()
    {
        var error = true;

        Console.WriteLine("SendGrid sending email");

        if (error) {
            Console.WriteLine("Error");     
            return false;
        }
        
        Console.WriteLine("Sendgrid email sent");
        return true;        
    }
}

public class MailJet: IEmailClient
{
    public bool send()
    {
        var error = false;

        Console.WriteLine("Mailjet sending email");

        if (error) {
            Console.WriteLine("Error");     
            return false;
        }
        
        Console.WriteLine("Mailjet email sent");
        return true;        
    }
}

dotnet fiddle


Solution

  • All applications have a Composition Root where the dependencies gets wired. Whether you write this manually or by configuring an IoC container the core of your code will most likely be identical.

    In your contrived example the Main method is the composition root and adding new IEmailClient implementations wouldn't impact the core of the application (EmailService), only the root.

    Now regarding the design, it's hard to tell, but perhaps you could have strived to apply the Composite & Decorator patterns to abstract away the existence of multiple clients. For instance:

    enter image description here

    The advantage of such design is that it allows to extend the behavior of EmailService without changing the class. You could add retrys, fallback, logging, etc. all without changing existing code. Furthermore, if EmailService ends up only delegating to IEmailClient you may even question whether that Facade is needed or not.

    Another advantage is that it lets you create clients with different behaviors on the fly. For instance, imagine the users of your system wants to be able to configure the fallbacks, retries, etc. then you could have a Factory that builds an IEmailClient instance dynamically according to their configuration.

    However, keep in mind not to overengineer the solution. You are already leveraging existing email gateways. If you aren't sure what you are going to need yet just strive for the simplest solution and refactor as needed.