API-first implementation

Introduction

API-first is an architectural design where the APIs of our application are given the first importance. The development revolves around the API that we will expose.

The result is that we have services that are developed by keeping the developer’s interest in mind. This type of architecture is suitable where different people are responsible for different services/modules and where parallel development of services is required.

Also check out the gRPC based implementation.

The traditional approach

Let’s consider there are only two modules in an application. inventory and order. The order module is dependent on the inventory module. This means, in the build configuration, order will have a compile time dependency on inventory.

In this setup, normally the inventory module will have a set of interfaces that will be exposed to the outside world. Developers are supposed to use these interfaces to call the required methods. Let us zoom in to the inventory module.

However, given the current dependency structure, no one is restricting us to use the *Impl class directly. I mean, we should not do it, but then this rule should be tied into the architecture itself. In the current situation, it is left to the goodwill of the developer.

One of the problem is that the consumer, order module in this case, is able to see all sorts of the implementation details, internal classes, helper classes among other things. The consumer is not at all interested in all those details. This will also require the developers to go through many unnecessary things in order to use the APIs.

Another issue is that it becomes relatively easy to create circular dependencies. Say, now for some reason, inventory needs to use certain APIs of the order module. It would be quite obvious in a two-module system that we have at present. But when there are 10, 15, or more modules, this can be quite hard to detect and debug.

Lastly, any changes in the inventory module are propagated into the order module. Even a slightest change in inventory forces recompilation in order. This leads to high coupling and increased build times.

API-first

Let’s see how can we address this problem.

First, we extract all of the APIs (externally exposed interfaces in this case) in a separate inventory-api module. This module contains only interface definitions and the immutable DTOs that forms the request/response structure.

Then, we add a compile-time dependency inventory-api to order module and a runtime dependency of inventory module. The inventory module now implements all the interfaces in the inventory-api module.

In gradle, for example, this can be achieved by using “implementation” for compile-time dependency and “runtimeOnly” for runtime dependencies.

Notice the dependency graph now. How the dependency on the inventory module is inverted. Instead of moving in to the module, it is now moving out. This dependency inversion principle is the D of the SOLID principles.

This way, the order module is decoupled from the inventory module. Changes in inventory module are not propagated to the order module. We need not compile order when there is an implementation change in inventory module. The possibility of cyclic dependency is eliminated and the developers of order module can start developing independently even if the inventory module is not yet completed.

Conclusion

We saw how the dependency inversion principle is not limited to classes. It can be applied at package, module, or service level as well. Inverting the dependency forms the basis of the api-first approach. It also helps in decoupling. The link to a sample implementation is here.

Leave a Reply

Your email address will not be published. Required fields are marked *