Dependency inversion is the best thing I've ever heard of cover image

Dependency inversion is the best thing I've ever heard of


thank you for teaching me this

2/16/2024, 12:17 PM

Reading time: 6 minutes

Supabase messed up our project

We, by that I mean me and my team working on a uni project, were using supabase as a neat backend solution for our web shop based on nuxt. After submitting it, we got positive feedback. It was working wonderfully. Until eventually, supabase decided to pull the plug on our project by changing how their API access works.


In this case, we were lucky our project was already graded and would throw it away anyway. But what if that wasn't the case? What if it was a project for an important client, and we would have to put in hours of work to fix it? What if the client was losing out on revenue because of this? It's clear that an approach like this is not sustainable.


Being dependant sucks

This is a clear example of how being dependent on a third party can be a huge risk. It's not just about the risk of the third-party going out of business, but also about the risk of them changing their API, their pricing, or their terms of service. All of these things are perfectly within their rights to do, but it can be a huge pain for the people using their services.


Keeping the core independent

Supabase is not the only SaaS company that provides such services, so switching to another one should be easy, right? Well, not really. We have supabase related code in almost every file of our project, and switching to another service would require us to rewrite a lot of code. So, there has to be a solution to this problem, right?


Yes, and that starts with writing your code in the "application core" (the part of the backend that we have full control over) in a way that is independent of the third-party services we use. Instead of calling the external API directly, we need a kind of middleware that we can swap out for another one if we need to. This is where the dependency inversion principle comes in.


The adapter metaphor

An example often used to explain the dependency inversion principle is the adapter metaphor. Imagine you have a device that needs to be plugged into a power outlet. Your own cable (belonging to the device) has a plug that doesn't fit into every outlet in the world. Plug depends on outlet So, you use an adapter that has a plug that fits into the outlet and a socket that fits your cable. This way, you can use your device in any country in the world. If you travel to a country with a different outlet, you just need to bring a different adapter. Cable Adapter


Applying the adapter metaphor to software

This concept can be applied to software as well. Instead of calling the external API directly (the outlet): Core depends on API ... we create an interface that our application core can use. In this interface, we can literally come up with any methods that we need to fit our application's needs (to fit the cable plug). Adapter depends on Core See the mock code below.

interface ExternalService {
    getProducts(): Promise<Product[]>;
    createProduct(product: Product): Promise<void>;
    // ...
}

Then, we create an adapter that implements this interface and calls the external API. Inside this adapter, we remap the parameters to fit the API (remap the wires to fit the outlet).

class SupabaseAdapter implements ExternalService {
    private supabase: SupabaseClient;

    constructor(supabase: SupabaseClient) {
        this.supabase = supabase;
    }

    async getProducts(): Promise<Product[]> {
        const { data, error } = await this.supabase.from('products').select('*');
        // ...
        return data;
    }

    async createProduct(product: Product): Promise<void> {
        const { error } = await this.supabase.from('products').insert(product);
        // ...
    }
}

If we need to switch to another service, we just have to create a new adapter that implements the same interface. The application code itself stays the same, other than changing the adapter, of course.

ExternalService adapter = new SupabaseAdapter();
const products = await adapter.getProducts();

Nice! We are now decoupled from the third-party service.


This is definitely worth the overhead, as the advantages are huge, and writing the adapter is not that much work.


Testing

Another benefit of this approach is that we can easily test our application core without having to call the external API. Say the API is providing data, but costs a few cents per call. We don't want to pay for that every time we run our tests. So, we can create a mock adapter that implements the same interface and returns some dummy data. This way, we can test our application core without having to call the external API.

class MockAdapter implements ExternalService {
    async getProducts(): Promise<Product[]> {
        return [
            { id: 1, name: 'Product 1', price: 100 },
            { id: 2, name: 'Product 2', price: 200 },
            // ...
        ];
    }

    async createProduct(product: Product): Promise<void> {
        // ...
    }
}

Again, our application code stays the same, but can be tested in isolation.


Continuous integration

This also makes continuous integration a lot easier. If we want to test a new API in production, we can just create a new adapter and use it for 10 percent of the requests. You can use dependency injection to switch between the old and the new adapter in the runtime. If the new adapter works well, we can switch to it completely. If it doesn't, we can switch back to the old adapter. This way, we can test new features in production without having to change the application code.


Where not to use this

The obvious answer is that you shouldn't use this when experimenting with code and coming up with ideas. Only in systems that will be used in production does this make sense. Another aspect to consider is the frontend. You could use the same principle to decouple the frontend from the backend, but this is not necessary, as you usually have full control over both the frontend and the backend. They can be dependent on each other without any risk. You wouldn't deliberately change the API of your own backend, right?


Conclusion

The dependency inversion principle is a powerful tool to make your application core independent of third-party services. It allows you to switch between different services without having to change the application code. It also makes testing and continuous integration a lot easier. Every developer should be aware of this principle and use it in their projects. It's worth the overhead.


Dependency inversion forms a large part of hexagonal architecture, which is explained in great detail here.

Quick links

BlogContact

Legal

ImprintPrivacy policy