API proxies are boundary services that act as intermediaries between a client and the microservices. Their goal is to encapsulate the microservice cluster, hiding the true origin of requested resources: this allows to further decouple the client from the server, allowing the latter to structurally change without breaking the client.

We implement two types of proxies: gateways and aggregators.

Gateways

The gateway pattern

A gateway is a type of reverse proxy that acts as the entry point into the application from everything that is outside the microservice cluster. A client performs a request to the gateway and it will see the response coming from the gateway as if it produced it; instead, the gateway re-routes the request to the microservice capable of handling it and returns to the client whatever response the microservice produces. It is similar to a façade in object-oriented programming.

The main reason a gateway is needed in a distributed application is that it hides the internal structure of the microservice cluster, as well as other things, such as the protocols that the services use. Imagine having a microservice exposed directly to the client, and maybe it starts growing as the application is being developed, to the point it is too large to be considered a microservice. Then you may want to split it in multiple parts, but you will not be able to do it because it would break any client that uses the service. An API gateway solves the problem: its contract does not change, it will only change the routing behavior in order to redirect the requests to the newly created microservices.

For relatively simple applications, a single gateway can be enough. However, as an application grows in size, usage and functionality, having only one gateway is impractical, as the application will need more and more features, like load balancing, API composition, authentication, protocol translation, etc. Let’s see how we can extend the gateway pattern introducting more and more specialized gateways.

Modelling a gateway

See the Modeling BFFs section.

BFFs

The BFF pattern

The Backends For Frontends (BFF) pattern comes into play when your server is accessed by more than one client. Suppose that your front-end team develops a SPA client and a mobile client. These clients will most often have different needs, which can’t be addressed easily by a single, general-purpose gateway. For example, the mobile client may need fewer data and use lighter DTOs than the SPA client, as well as being involved in completely different data flows and operations. As a result, the one gateway will bloat with all these functionalities and the team working on the gateway will have a poorer development experience.

One way to address this is to have one gateway per user experience. Each gateway, which has now become a back-end for front-end, will be specialized on a single user experience which, however, may be the same for multiple clients (for example, an iOS app and an Android app might use the same gateway if their needs are similar).

Using the BFF pattern provides other benefits. The gateways are separated from each other, which improves reliability: if one of them fails, it does not affect the other. It also improves availability, because there is more than one gateway process running. Observability is also improved, because calls to the back-end are separated and grouped by client type.

Modeling BFFs

The model of a gateway is very simple. Just provide a name, a description and a list of services it will grant access to. The bff.yaml file will contain the list of the gateways that will be generated.

- name: Mobile
  type: gateway
  description: Mobile BFF
  services:
    - Couriers
    - Logistics
- name: SPA
  type: gateway
  description: SPA BFF
  services:
    - Store
    - Logistics

Aggregators

The aggregator pattern

In a microservices architecture, situations where more than one service is involved in a single operation are not rare. Often, data needs to be retrieved from different microservices and combined. While this operation can be performed by a client, sometimes it is better to have a separate service of the cluster doing this instead.

Suppose that a client needs to combine data objects coming from three separate services to display them in a single page. In some cases, this might end up in causing performance issues that will result in poor user experience. The internet has much higher latency than a LAN. Although this may not be a problem when the three requests can be performed in parallel, there are occasions where requests need to be issued sequentially; in such scenarios, having a dedicated service performing this work can speed up dramatically the whole operation. Moreover, implementing API composition inside the client code is a distraction for a client developer, whose main task is to create a great user experience.

Modeling aggregators

An aggregator is a service like any other, albeit being more simple than a regular service and more complex than a simple gateway. Aggregators don’t have a domain model and, therefore, database entities. Here is an example of how to model an aggregator.

name: MyAggregator
type: service
aggregator: true
description: Aggregates some services
namespace: Ca.ShoppingCart.MyAggregator
services:
  - Store
  - Logistics
contracts:
  - name: Product
    description: Product entity
    type: entity
    fields:
      - name: name
        type: string
        description: The name of the product
      - name: description
        type: string
        description: A description of the product
      - name: price
        type: decimal
        description: How much the product costs
      - name: imageId
        type: uuid
        description: The id of the image representing the product
      - name: imageDescription
        type: string
        description: The description of the image representing the product
operations:
  - name: getProductWithWeight
    type: http_get
    description: Gets a product with its weight
    parameters:
      - name: id
        type: uuid
        description: The id of the product
        direction: in
      - name: product
        type: Product
        description: The found product
        direction: retval
      - name: weight
        type: float
        description: The weight of the product

This YAML generates a service with out-of-the-box proxy classes that can be used to access the microservices listed in the services section of the YAML, as well as DTOs and controller methods as in any other service.