Wait...what's that doing here, that's not even an OOP design pattern...?
That's true: Compared to most other patterns in this catalog, the API Gateway might appear a bit out of place here. While most classic OOP patterns are primarily defined at the class level, the API Gateway describes the interaction between components within a distributed system, therefore using a much higher level of abstraction.
But still, its wide acceptance in today's microservice systems makes it worth discussing and a good candidate for every design pattern catalog worth its salt.
The architectural foundation of this pattern is relatively straightforward to understand and follow. However, implementing a complete API Gateway with monitoring, authentication, and load balancing can be a complex task.
We'll start our journey with a quick look at some characteristics of microservice systems and their potential downsides and then look at how an API gateway can help overcome these limitations.
A typical microservice system consists of dozens or even hundreds of small services, each one representing an isolated bounding context. Luckily, for demonstration, we will take a much smaller system.
For our example, we take a small online shop with a web-based frontend and three microservices in the back, each encapsulating specific domain knowledge. As with most examples, this arrangement is not a good choice for a real-world scenario but should hopefully be sufficient for us now.
In our use case, a customer has chosen an item from our product range and wants to start the checkout process. For this, our system creates an Order entity with the products to purchase. Additional data like the customer's name and address are also required for creating the web-based form where the user can view the order details and proceed with the checkout.
Since each of our services is only responsible for a single part of this task, our client application must communicate with all of them to request the required information.
Please note that in our example, we follow a strategy of loose coupling between our services. While this makes our services relatively independent, it also means that the client is responsible for collecting and merging all data itself. We could change this behavior by increasing inter-service communication or storing data redundantly. However, we've omitted this to keep our example simple.
While the system follows some general microservice guidelines - like data isolation or lightweight communication - it also has some significant drawbacks:
- Each client has to communicate with each service individually. This makes our system less flexible as any architecture change must also be propagated to all client applications.
- If our client is a website or a mobile application, every service must be publicly available to be accessible. This can present a potential security risk.
- If the service APIs are very fine-granular, clients must make several calls to collect and compose data from different services. This impacts the performance and network traffic of the system.
- Every service needs separate authentication and authorization logic. This exposes additional security risks.
- Services may use different protocols like HTTP, gRPC, websockets or something else. Our clients must support all of these protocols if they want to talk to these services.
Most of these issues can be handled for smaller systems but get really hard to implement or fix if the number of services grows. Finding a solution for these issues definitely saves you time and resources in the long run.
The Microservice API Gateway pattern is such a solution.
Maybe you've heard already of the Facade? It's a widespread OOP design pattern that controls access to a subsystem by providing a single entry point for all external clients. From its structure, the API Gateway looks very similar, but on a network level.
An API Gateway is an additional network component between your clients and the backend services. Instead of communicating directly with your backend, Clients send their calls only to the API Gateway. There, the incoming requests could either be processed directly or routed forward to the underlying services.
The API Gateway essentially fulfills three tasks:
Its primary function is that of a Reverse Proxy. The name comes from its behavior, which mirrors a regular network proxy but with reversed direction. Both are located between an internal and external network. While the regular proxy controls outgoing calls to the external network, a reverse proxy handles all incoming requests and sends them to the correct internal service.
Using a reverse proxy significantly reduces the coupling between the internal network and outer clients, allowing it to change the internal configuration and structure without notifying the clients. Clients only have to remember the proxy's address instead of keeping track of all service locations.
Data and Message Aggregation
The granularity of microservice APIs often differs from that required for the actual use case. On the one hand, APIs could return more data than needed, which unnecessarily increases the message payload in the network. On the other side, if the data required by a use case is split over several services, the client has to make many calls and compose the incoming requests. This leads to more network requests and creates additional overhead as more messages have to be transferred.
Look, for instance, at our previous example: Instead of a single API call, our client had to request data from different services and merge the responses for the final result set. This increases the network load and unnecessarily exposes data to the network that is not even required for our concrete use case. An API Gateway can provide a more use-case-specific API tailored for each client. The gateway handles all calls to the subsystem and returns only the aggregated results to the clients.
This relieves the client network from large parts of the communication, which can be especially beneficial for cellular networks or networks in remote areas with limited capabilities.
API Gateways are also a good place to handle cross-cutting concerns like authentication, logging, or monitoring. Without the gateway, these aspects would have to be replicated in each microservice, cluttering their implementation with domain-unrelated logic. Especially critical security features like authentication or authorization are better not re-implemented for every service as this introduces the risk of potential bugs or using an outdated library.
Keeping the microservices free of all these concerns and giving them a clear focus on the domain logic makes it also easier to replace or rewrite them later if necessary.
However, placing all the infrastructure logic into the API Gateway is not without risk: If the gateway has too many responsibilities, it can become very complex and hard to maintain. This can result in the literal Big Ball of Mud, but in this case, not on code but on a network level.
There are already a few API-Gateway implementations available, most of them distributed under open-source licenses. Rolling out your own creation is thus rarely necessary unless you have very specific requirements.
Zuul from Netflix might be one of the most prominent API Gateway implementations out there. At its core, Zuul's functionality is based on several filters that can be executed during different request phases, like before a request arrives or after it was already routed. This filter mechanism can be used to implement cross-cutting concerns like authentication or logging. Netflix provides Zuul integrations for some of their other network components, together with a small reference application.
Spring Cloud Gateway is another Java-based API Gateway implementation. It supports request routing to provide reverse-proxy functionality and also follows a filter-predicate model similar to that used by Zuul for further cross-cutting concerns.
Compared to the previous examples, Nginx might look slightly minimalistic at first sight. It is mainly a webserver with additional functionalities like load-balancing and request routing. However, when we need an API Gateway only for its primary purpose, acting as a reverse proxy, then Nginx may already be sufficient and could even be easier to setup and configure. But as always, there is no free lunch: While your basic setup might be easier, ensuring that all your additional components run smoothly together can require some configuration effort.
KrakenD is another open-source API Gateway engine that provides reverse proxy, aggregation functionality and other concerns like logging or authorization. It is based on the Lura framework, which is also an API Gateway implementation, but KrakenD extends it with additional middleware. Most customization can be applied through configuration files, but there is also the option to implement additional plugins in Go.
Of course, this list is by no means exhaustive. Its aim is more to prove that there is a variety of different implementations available. The choice may depend on your requirements or skills and sometimes even on your personal language preferences.
This API Gateway stuff sounds really good, but what are the pitfalls?
The API Gateway is a great tool, but there are some things to consider.
Since it plays a central role in the network, the temptation is high to outsource more and more tasks there. Especially the aggregation of data bears the risk of moving too much domain logic out of the services and into the gateway. If you find yourself piling up business logic in your gateway, you better create a separate domain service for this. Often, this can indicate that your bounding contexts do not align well with your use cases, and you might have to reorganize your domain design.
Having an API Gateway can also make your system less robust. The advantages of a microservice architecture lie in its decentralization and loose coupling. Adding an API Gateway, on the other hand, introduces a potential single point of failure to the system. If your gateway collapses, it will take all connections between the clients and the backend with it. This can be avoided by using additional redundancies and having more than one API Gateway instance available.
Are there any variants or modifications to this pattern?
The Backend For Frontend is a more specialized version of the API Gateway. In this variation, every client talks to its own dedicated API Gateway that supports precisely the functionality it requires. This can make sense since different clients can have different needs. A mobile app, for instance, might request less or other data than a web-based client. Using a separate Backend For Frontend can optimize the communication between your clients and your backend, but remember that additional components also require extra maintenance efforts.