Why wrapping 3rd-party calls to external services is always a good idea

Thu Mar 31 2022

When using a 3rd-party package that makes calls (or API requests) to an external service it is always a good idea to wrap 3rd-party service with a thin wrapper (Facade). Why?

Wrapping it avoids vendor lock-in and tight coupling. Imagine that the API the 3rd-party is using stops working. Now you need to change service calls across your whole project. If wrapped, you only need to modify your code once.

Another reason is that you can then build and setup the 3rd-party service (or the client) according to your requirements.

Let me give you an example:

...

use Kourses\Website;
use Mpociot\VatCalculator\VatCalculator;

...

class VatHelper
{
    protected $calculator;

    public function __construct(Website $website)
    {
        $calculator = new VatCalculator();
        $calculator->setBusinessCountryCode($website->user->country);

        $this->calculator = $calculator;
    }

    ...
}

In my project, I'm using driesvints/vat-calculator package to calculate VAT (value-added tax) rate and to validate the EU VAT number. EU VAT number validation is being done by calling an external API.

As VAT rate calculation needs to take into account your business location, I need to provide the business country code when setting up the calculator. As a product I'm working on is a SaaS, I need to specify this param on each request and can't do it through configuration files (which this package supports). So I'm "building" up the VatCalculator according to my requirements.

If I did not wrap this service, I would have to configure it each time I call it. Not a big deal I know, but this is a simple example.

Besides doing a simple "wrapping" I've added a couple of methods that slightly modify 3rd-party service behavior.

...

use KoursesMember;
use KoursesErrorsVatValidationException;

use Exception;

class VatHelper
{
    ...

    public function isValidVatId(string $vatNumber): bool
    {
        try {
            return $this->calculator->isValidVATNumber($vatNumber);
        } catch (Exception $exception) {
            throw new VatValidationException("VAT validation failed.", 500);
        }
    }

    public function getTaxRate(string $countryCode, string $postalCode = null, string $vatNumber = null): float
    {
        $isCompany = $this->isValidVatId($vatNumber);

        return $this->calculator->getTaxRateForLocation($countryCode, $postalCode, $isCompany);
    }

    public function getTaxRateForMember(Member $member): float
    {
        return $this->getTaxRate($member->country, $member->zip, $member->vat_id);
    }
}

In method isValidVatId I'm catching a 3rd-party service exception and re-throwing it using a message (and code) that is in sync with my app. Now I don't need to catch 3rd-party package exceptions in my controllers.

If I decide to change this 3rd-party package I wouldn't have to go through my code and change all mentions of this package.

In getTaxRate method, I've also slightly altered behavior which suits my flow better. I'm validating the VAT number before getting the tax rate for the user's location (as it is dependent on your business location as well as your customers' location).

Method getTaxRateForMember is a simple helper which allows me to easily pass my $member object and get its current VAT rate without needing to "spread" required params.

Using this technique you could easily implement caching in front of external service calls if need be. Here, as the VAT number checked should be different each time I'm not doing that.

This wrapper borrows from a Facade design pattern, a Proxy, and even a Decorator. If you wish to learn more about design patterns be sure to check out https://refactoring.guru/design-patterns as they have a nice overview of basic patterns as well as real-world examples.

If you like this article consider tweeting or check out my other articles.