The importance of abstracting third party services

John Richardson

A pragmatic software developer with ~ 10 years experience. PHP/Vue JS advocate.

@JRdevelop January 22, 2021

In 2018 it’s commonplace to integrate our apps with 3rd party services. The SAAS (software as a service) marketplace is booming and there is seemingly a service for anything and everything. Not that this is in anyway a bad thing. Having the option to outsource infrastructure or fulfil software requirements, often at the touch of a button, can certainly prove useful. It affords us the freedom to invest money and time into the specific problem at hand, rather than investing into more generic problems or problems that have already been solved. For example, rather than implementing our own authentication logic we can outsource to Auth0, or rather than creating a CMS & Content API we can outsource to Contentful.

While these third party services are powerful and have a lot to offer they do, as with many things in life, have some disadvantages. In using a 3rd party service you will often lose the ability to fully customise certain parts of your application for example. Another problem being that these services are often not shy about raising prices. As such it’s important that we do not tightly couple our applications to any given service. In the real world things do go wrong, apps outgrow the 3rd party apps they depend on, better options become available, companies go out of businesses and the list goes on. Fortunately, in most cases it’s realistic and often rather simple to avoid tight coupling by having an abstraction layer sit between 3rd party services and your application.

What is an abstraction layer?

An abstraction layer is used to mask the specific details of some functionality, usually with the aim of exposing only a subset or a simplified version of that functionality. In other words, you add your own interface in front of an existing set of functionality. Once the new interface is in place the rest of your application interacts with it exclusively – it never touches the set of functionality behind it without going through the interface. This could look as simple as a single function, a single class or it could be something more complex.

In adding such an abstraction layer, it becomes possible to interact with some functionality without the application becoming coupled to it. It also makes it possible to hide complexity or to simplify the interface. Which is obviously quite useful.

But what specifically is it? What does it look like? Well it can be as simple as a single class, or a set of classes. It could even be a single function. As long as the rest of your application interacts with the abstracted service/functionality through the newly added interface it has been abstracted away – so to speak.

Let's look at an example:

class AuthMagicService
{
    public function auth($username, $password) : ThirdPartyUser
    {
        //check authentication

        //get user
        $user = ThirdPartyUser::find($username);

        return $user;
    }

    public function resetPassword($username, $password, $newPassword) : bool
    {
        //resets password

        return true;
    }
}

To abstract this away, and to prevent your application from becoming coupled with this code you’d need an abstraction. Here’s a working example (the code here is for demo purposes only and I’d not recommend using this for authentication):

class MyAppUser {
    public $username;
}

interface AuthServiceInterface {
    public function authenticate(string $username, string $password) : MyAppUser;
    public function resetPassword(string $username, string$password, string $newPassword) : bool;
}

class MyAppAuthMagicService implements AuthServiceInterface
{
    /**
     * MyAppAuthMagicService constructor.
     * @param AuthMagicService $authMagic
     */
    public function __construct(AuthMagicService $authMagic)
    {
        $this->service = $authMagic;
    }

    public function authenticate(string $username, string $password) : MyAppUser
    {
        $authMagicUser = $this->service->auth($username, $password);

        if (!$authMagicUser) {
            throw new Exception('Unable to find a user with those details');
        }

        //In reality you'd probably do something like
        //mapping the third party model to your own, or
        //retrieve the user from the local DB.
        $user = new MyAppUser();
        $user->username = $authMagicUser->username;

        return $user;
    }

    public function resetPassword(string $username, string $password, string $newPassword) : bool
    {
        $authMagicUser = $this->service->auth($username, $password);

        if (!$authMagicUser) {
            throw new Exception('Unable to find a user with those details');
        }

        return $this->service->resetPassword($username, $password, $newPassword);
    }
}

In a nutshell the class:

  • Takes the third party service (AuthMagicService) as a constructor parameter and exposes all of it’s functionality through it’s own interface. Of course it’s not always necessary to expose everything, sometimes you may only need a subset of the functionality offered by the third party.
  • Implements an interface (AuthServiceInterface). This is important when abstracting away third party services as it makes switching to a new service easier since:

    • The methods that the new service needs to implement are well documented.
    • Anywhere the existing service is used, the new service can be used since they are guaranteed to share the same methods.
    • If you’re using a DI/IoC Container (or Laravel’s Service Container) it’s possible to switch over to the new service with a single line of code. For example, if you were using Laravel you’d be able to just switch the binding of the interface to the new class.
  • Has been designed in such a way that the underlying functionality is never exposed. Notice how the ‘authenticate’ method returns a MyAppUser instance rather than a MagicAuthUser instance since the latter would result in tight coupling. For further info read up on leaky abstractions.

With the above abstraction in place, moving to a new service would simply involve:

  • Creating a new class which implements AuthServiceInterface.
  • Switching out all uses of the old class with the new, which could be as simple as changing one line of code.

All of the code built around and on top of the authentication abstraction would still function just fine and would not need to be changed. If on the other hand, the application had been interfaced directly with the AuthMagicService it would likely involve changes to various parts of the code base. For example, image the application had a User profile controller that:

  • Made a call to authenticate() to get the ThirdPartyUser instance.
  • Called various methods to retrieve further data, for example getFriends(), or getLastLogin()
  • Passed the ThirdPartyUser to a logger
  • Passed the ThirdPartyUser to a view

When the time came to remove MagicAuth, the ThirdPartyUser model would go with it. We’d likely have to change the controller, the logger, the view and possibly more. We’d also have to ensure that our new model shared the same functionality (getFriends or getLastLogin) or we’d have to do further refactoring. Either way would involve additional work, which depending on how different the new service is, could prove hugely problematic.

What are the benefits?

When writing an abstraction layer I usually have one of two things in mind. I either want to simplify the interface or I want to make sure my application is loosely coupled to a particular service. Let’s look at each in further detail.

Preventing tight coupling

As previously demonstrated, when you add a third party service to your application and start interacting with it you quickly become coupled to it. This may not prove problematic initially, but if the costs of that service rise or if you outgrow it, replacing it is going to be difficult, time consuming and you run the risk of introducing new bugs to your application.

With a minimal time/effort investment upfront you can avoid this tight coupling by implementing an abstraction.

Simplifying a set of functionality.

Sometimes we need to solve complex problems and in doing so we end up writing complex code. At other times our human side takes over – perhaps we’re tired or overworked – and we write bad, overly complex code to solve relatively simply problems. And at the worst of times we need to use other peoples poorly written code. Whatever the reason for complex code, the end result is almost always results in an increased cognitive load. This undoubtedly has an impact on developer performance/output and is especially true for high impact parts of the code base, or for WebDev shops which have high developer turnaround. In such cases, it’s often worth hiding the complexities behind an abstraction.