Luiz BrancoEngineering @ Manifold

How Go interfaces can facilitate switching external services

How Go interfaces can facilitate switching external services

A couple months ago we at Manifold replaced our email service provider from SendGrid to Mailgun. This post explores how we leveraged Go interfaces to make the switch easier, improve our tests and, instead of a complete replacement, we ended up using the older service as a fallback.

Why changing email services in the first place

Around October 2017 we started a project at Manifold called dogfooding. For those not familiar with the term, dogfooding — also known as eat your own dog food — is when a company starts using its own product or service. The basic idea is that if you have a good product and you are part of your product’s demographics, you probably should be using your own product in the first place!

The main benefit of dogfooding for me is that it inspires trust about the product, specially inside the company. If we built something useful and that we are proud of, we will do our best to keep improving it. Dogfooding also provides a very fast feedback loop for new features and also keep us in check, if some aspect of the product is lacking, we are one of the first to feel the pain.

However, sometimes dogfooding is not possible right from the start. A company needs to build a foundation first before its product reaches a stage that it can be used. That was the case for Manifold: we needed SendGrid in the beginning to send transactional emails to users. Once our marketplace went live and Mailgun was a part of it, then we had a perfect scenario to use Mailgun instead.

Brief summary of Go interfaces

Interfaces in Go are very similar to other languages: it declares zero or more methods a type must implement in order to be used in place of that interface. To give a simple example, let’s say we have an interface for things that can float in water. In Go that would be declared as:

type Floater interface {
  Float()
}

This means that any type that implements a Float method with no arguments and no return is considered to be a “Floater”. Now we define two other types that implement the same method:

type Duck struct {}

func (Duck) Float() {}

type Witch struct {}

func (Witch) Float() {}

Anywhere in our code that requires a Floater both a Duck or a Witch can be used instead. Note that we don’t explicit say that those types implement the Floater interface, but the compiler will enforce that if we try to use another type that doesn’t have the same method signature.

Go’s interface implementation being implicit is what makes it different from other languages. A drawback is that you sometimes have to rely on the documentation of the type to understand which interfaces it implements.

The main advantage, however, is that you can define an interface for a type that your package is importing. For instance, if there is a third party library with the following type:

package other

type Wood struct {}

func (Wood) Float() {}

Wood could also be used as a Floater even if the other package is not aware of your interface.

Replacing the old API

The first question you should ask is: why do we care about interfaces at this point? Why not just replace the old API with the new one everywhere it’s being used and be done with it? And that’s a fair question.

The main reason is to decouple our system from a single implementation of a third-party service, ie: the system should know how to send emails, but it shouldn’t be concerned whether we are using SendGrid, Mailgun or Amazon SES. That makes easier to switch implementations in the future and also to fake an implementation for tests when we don’t need to send real emails.

SendGrid Go library is:

type Client struct { … }

func (*Client) Send(email *mail.SGMailV3) (*rest.Response, error)

Both mail and rest are also part of their library. In essence, SendGrid client expects a custom email format and returns a custom response or an error. In our system we use our internal Email type to represent an email message and then translate that to SendGrid format.

Our new interface can be defined as:

type Mailer interface {
  Mail(Email) error
}

Since we don’t care about the response, only if there was an error, we can drop that from the interface. Everywhere we were using the SendGrid *Client we can now replace with the Mailer interface.

For example, if we had:

Embedded content: https://gist.github.com/luizbranco/e6e9735a049f20b4b0afef7ddf9bd205#file-user-go

We can replace with:

Embedded content: https://gist.github.com/luizbranco/5699dc9c6ad30ded3e86c59d70de3e78#file-user-go

Of course, that will break our code because SendGrid’s client doesn’t implement the mailer interface yet.

Go doesn’t allow us to add a method to a type that doesn’t belong to the same package, but we can use composition to extend Client with a new method:

Embedded content: https://gist.github.com/luizbranco/c6fb1f890c9dab171441171ab8c9fc88#file-sendgrid-go

Our own SendGridClient type implements the Mailer interface and the code compiles again. We can use the same approach for Mailgun:

Embedded content: https://gist.github.com/luizbranco/c6ce59c274d78342ed2b12d9d2cefd4d#file-mailgun-go

Both Mailgun and SendGrid implement the Mailer interface and can be used interchangeably!

Using a fallback implementation

Now that we have two implementations of the Mailer interface we have to decide which one to load when a mailer is needed. A simple way to choose one at startup is if we have env variables needed for a service.

Embedded content: https://gist.github.com/luizbranco/441f5075ad1f800579fc0a198dce0352#file-mailer-go

The downside of this approach is that the application requires a restart in order to switch email services. Another solution could be tracking failures and switch implementations if there is a high number of errors during a given period of time.

How interfaces facilitate tests

Interfaces are an easy way to decouple your application from external services because they don’t assume how the API is been implemented. For the same reason they are also very good for testing. During unit tests we can define our own email testing service instead of calling the real implementation.

Suppose that we are writing tests for the UserActivation function we showed before:

func UserActivation(m Mailer, e Email) error {
  return m.Mail(e)
}

Instead of using one of our real clients, we can define a testing version that stores all the messages passed to it or returns an error:

Embedded content: https://gist.github.com/luizbranco/ffe52df6a96102c4cc3f5e3f870ca73f#file-mock-go

During tests we can assert that the message we are planning to send was added to the TestingMailer queue or an error was returned.

Embedded content: https://gist.github.com/luizbranco/96e7e6cde604d23772e59688010fff71#file-test-go

Conclusion

I hope with this post to have demonstrated how useful interfaces are in Go, how to use them to decouple an API from its implementation and how to mock an interface during tests.

As a rule of thumb strive for small interfaces, if your interface has too many methods there are probably smaller interfaces that could be extracted from it. An interface can also be composed of multiple interfaces, Go’s io package is a great example of that, and a type can implement multiple interfaces simultaneously.

StratusUpdate

Sign up for the Stratus Update newsletter

With our monthly newsletter, we’ll keep you up to date with a curated selection of the latest cloud services, projects and best practices.
Click here to read the latest issue.