When it comes to the design of APIs for our Micro Frontend Architecture we usually have to deal with the decision to go for generic or consumer-driven Backend Systems. Both of them have their advantages, and the Backends for Frontends Patterns allows us to use both of them.
Introduction
When we start to design APIs for Micro Frontends we usually have to make the decision, if we want our endpoints to be (i) generic with a high priority on reusability, or if we want to define a contract (ii) tailored for a specific set of consumers. At clients, we tend to call the first approach generic APIs and the second one consumer-driven APIs. As usual, both flavors have their benefits but also their particular downsides.
Generic APIs
First of all, it is important to mention that generic APIs are usually very hard to achieve on the hoof, as they require a deep understanding of the domain we want to model endpoints for. Achieving this adequately usually takes time. In reality, we observe two more major drawbacks when it comes to frontend applications consuming this kind of APIs: (i) over-fetching and (ii) over-requesting. Over-fetching describes the problem of receiving more data than actually needed, over-requesting labels the problem of having multiple requests to finally achieve the needed data-aggregation.
Over-Fetching
Let's image the simple example of a user-service that provides our frontend application user information: This user-service, responds a user-model consisting of first name, last name, and the history of orders (Listing 1). The requirement in our given scenario is to build a dashboard that should greet the user with his first name. As we only use one field of the entire model, the rest of the data is unnecessarily fetched. In short: We do over-fetching.
Listing 1: User Model with orders included.
Over-Requesting
A solution to this problem of over-fetching could be to extract the orders, by following a Restful HATEOAS2 approach and just linking the orders (Listing 2). But also this approach has its weaknesses. In a situation where we have another micro-frontend, which should show all the orders of a customer. We need to first fetch the user and then fetch each existing order. This over-requesting introduces a lot of complexity on the client-side in aggregating data and handling errors. In addition, it can lead to tremendous performance issues.
Listing 2: User Model with externalized orders.
Consumer-Driven APIs
In this opposite approach of generic APIs, we follow the idea of giving the client exactly what it needs. In the scenario described above, this would lead to a user-service that returns a greeting and an order history in the same way as we later require it in our application. This makes the consumption of course very easy and avoids over-fetching and over-requesting in the short term, but usually leads to an unmanageable number of endpoints, which will always be articulated in maintenance issues on the long run. One of the symptoms is to usually end up with a lot of dead endpoints, that are not consumed anymore.
Backends for Frontends
The idea of the Backends for Frontends (BFF)1 pattern is to get the best of both worlds: APIs that can be easily reused but do not lead to over-fetching or over requesting on the client. As the name already implies, we do this by creating a dedicated backend for our frontend service. This Backend for Frontend is responsible to bring data exactly into the shape as it is needed by the client application.
As shown in Figure 1 each Frontend has it's on service running on the backend, which is only called by one specific Frontend Application. Even if the concept looks straight forward at first glance, there are a couple of properties that need to be considered:
As we tailor backend solutions to specific frontend needs, let's not overlook the significance of visual presentation of our content. Learn how integrating Open Graph Images with Gatsby can elevate your content's visibility on social media.
Generic Domain Services and Specific Applications Services
While the Backend for Frontend provides a Consumer-Driven API, exactly how the clients need it, the Domain Services can still stay generic and therefore allow easy reuse from multiple consumers. In terms of reusability, I usually phrase it as: Keep domain services as generic as needed and make your Backend for Frontends as specific as possible.
The BFF is part of the Client
This one is critical. More specific this usually means that the BFF and the Frontend are one deployment unit. Once you change the BFF you usually need to change the Client and vice-versa. Therefore the same team that is responsible for the Client should also be in ownership of the BFF. In our experience it made sense for Web Applications to build the BFF in the same technology as the consumer, mainly for three reasons:
- Minimize context switch for the team working on the BFF and the Client. In Angular, we usually use NestJS4 therefore or Express for React as the mental switch needed for those frameworks is minimal.
- As the main purpose of the BFF is to reshape and aggregate data, which is quite simpler in a non-verbose typed language, we prefer Typescript or Javascript over Java or C#.
- Contracts can be defined directly via interfaces, e.g. in Typescript, instead of providing contract definitions like Swagger. That simplifies changes.
As BFFs and Clients are strongly related, are one deployment unit, and mostly change together, putting them into the same repository makes mostly totally sense. We will go into detail about this in the article about monorepos of this series.
BFFs don't have to be classical Services
When I explained BFFs a couple of years ago, I implicitly always explained them as classical microservices within our system. Somethin we tend to create in ExpressJS, NestJS or Spring Boot. We realized that this is no longer true, for example, Gateways or Data Query Services like GraphQL can be reasonable alternatives under specific circumstances.
The BFF should only cover presentation logic
If we think of applications in a very (very) abstract way, we can summarize four levels of logic: (i) persistence logic, which stores and loads data somewhere, (ii) domain logic, which for example calculates based on a birthday if somebody is allowed to vote or not, (iii) flow logic or composition logic, which usually describes how components interact with each other. For example, if we call a service A, it decides based on parameters or internal state if service B, or service C should be called in the further course. And last but not least, (iv) presentation logic, which brings data into the shape as we need it in our UI.
It is essential for the success of architectures following the BFF pattern to only handle the presentation logic (iv). Flow logic (iii) cannot always, but should mainly be avoided, as we otherwise end up in duplicating this logic. And as the flow logic through the different components within our system is usually not frontend related, but rather a domain specific logic (e.g. opening a bank-account or finalizing an order), they should not be part of the non-reusable Backend for Frontend.
As usual, there is of course space for exceptions: For example, saving UI specific settings could be a valid case for persistence in the BFF. But in general, it should be stated: A Backend for Frontend should focus on presentation logic.
Conclusion
Backends for Frontends turn out to be a very useful approach for making frontend applications more resilient against changes in our domain services, but without introducing problems like over-fetching and over-requesting. In our daily business of consulting companies in micro-frontends and micro service architectures, we found few cases were they were not worth considering: As the overhead can usually be outweighed by the benefits gained. The strategic implementation of Backends for Frontends marks a pivotal step in enhancing micro frontend architectures. As we conclude, it's worth noting how such tailored solutions can significantly elevate your product delivery processes. Explore our Product Delivery services to leverage bespoke backend solutions that align perfectly with your frontend needs.
The application of Backends for Frontends is a real-world proven way3 to solve a pressing concern when utilizing a micro frontend architecture which is built upon the concept of generic and reusable microservices. It's also worth mentioning that there are plenty of use-cases besides the implementation of micro frontends where the application of the Backends for Frontends pattern could be considered 1.
The simple act of limiting the number of consumers they support makes them much easier to work with and change, and helps teams developing customer-facing applications retain more autonomy. – Sam Newman
[.blog-divider][.blog-divider]
- REST is defined by four interface constraints: identification of resources; manipulation of resources through representations; self-descriptive messages; and, hypermedia as the engine of application state (HATEOAS)
- The term Backends for Frontends was coined by Sam Newman, one of the main contributors to formalize the microservice architectural style. Newman described it in the context of each BFF serving a different device.
- NestJS is a progressive Node.js framework for server-side applications mainly influenced by concepts used in Angular.
- Lukasz Plotnicki in a blog post about the usage of the Backends for Frontends Pattern applied at soundcloud.