Microservice architecture has long been the de facto standard in the development of large and complex systems. It has a number of advantages: strict division into modules, weak connectivity, resistance to failures, gradual release of production and independent versioning of components.

Often, when speaking of microservice architecture, only the backend architecture is mentioned, and the front-end as it was, remains monolithic. It turns out that we did a great backing, and the front pulls us back.

Today I will tell you how we did the microservice front in our SaaS solution and which problems we encountered.

Issue

Initially, the development in our company looked like this: there are many teams involved in the development of microservices, each of which publishes its own API. And there is a separate team that is developing SPA for the end user, using the API of different microservices. With this approach, everything works: microservice developers know everything about their implementation, and SPA developers know all the intricacies of user interactions. But there was a problem: now each front-end should know all the intricacies of all microservices. There are more and more microservices, more and more front-end providers – and Agile begins to fall apart, as specialization appears within the team, what means, that interchangeability and universality disappear.

So we came to the next stage – modular development. The frontend team was divided into subcommands. Each was responsible for its part of the application. It has become much better, but over time this approach has exhausted itself for several reasons.

  • All modules are heterogeneous, with their own specifics. For each module, its own technology is better suited. At the same time, the choice of technology is a difficult task in the conditions of SPA.
  • Since the SPA application (and in the modern world this means compiling into a single bundle or at least an assembly), only the entire application can be issued at the same time. The risk of each extradition is growing.
  • It’s getting harder to do dependency management. Different modules need different (possibly specific) dependency versions. Someone is not ready to switch to the updated dependency API, and someone cannot make a feature due to a bug in the old dependency branch.
  • Due to the second point, the release cycle for all modules must be synchronized. Everyone is waiting for the laggards.

„Cutting“ frontend

The moment of accumulation of critical mass came, and we decided to divide the front-end into … front-end microservices. Let’s define what a front-end microservice is:

  • a completely isolated part of the UI, in no way dependent on others; radical isolation; literally developed as a standalone application;
  • each front-end microservice is responsible for a certain set of business functions from start to finish, so it is fully functional in itself;
  • can be written on any technology.

But we went further and introduced another level of division.

Fragment concept

We call a fragment a bundle consisting of js + css + deployment descriptor. In fact, this is an independent part of the UI, which must comply with a set of development rules so that it can be used in a general SPA. For example, all styles should be as specific as possible for the fragment. There should not be any attempts at direct interaction with other fragments. You must have a special method to which you can pass the DOM element where the fragment should be drawn.

Thanks to the descriptor, we can save information about all registered fragments of the environment, and then have access to them by ID.

This approach allows you to place two applications written on different frameworks on one page. It also makes it possible to write universal code that will allow you to dynamically load the necessary fragments on the page, initialize them and manage the life cycle. For most modern frameworks, it’s enough to follow the “hygiene rules” to make this possible.

In cases where the fragment does not have the opportunity to “cooperate” with others on the same page, there is a fallback script in which we make the fragment in an iframe.

All what a developer who wants to use an existing snippet on a page needs to do is:

  • Connect the microservice platform script to the page.
  • Call the method of adding a fragment to the page.

Also, for communication between fragments, there is a solution built on Observable and rxjs. It is written on NativeJS. In addition, the SDK comes with wrappers for various frameworks that help to use this solution natively. Example for Angular 6 is a utility method that returns rxjs / Observable:

In addition, the platform provides a set of services that are often used by different fragments and are basic in our infrastructure. These are services such as localization / internationalization, authorization service, work with cross-domain cookies, local storage and much more. For their use, the SDK also provides wrappers for various frameworks.

Combining the frontend

For an example, we can consider this approach in the SPA admin area (it combines different possible settings from different microservices). We can make the contents of each bookmark a separate fragment, each microservice will be delivered and developed separately. Thanks to this, we can make a simple “header” that will show the corresponding microservice when clicking on a bookmark.

Developing the idea of ​​a fragment

The development of one bookmark with one fragment does not always allow us to solve all possible problems. It is often necessary to develop a certain part of the UI in one microservice, which will then be reused in another microservice.

And here fragments help us too! Since all the fragment needs is a DOM element for rendering, we give any microservice a global API through which it can place any fragment inside its DOM tree. To do this, just pass the fragment ID and the container in which it needs to be made. The rest will be done by itself!

Now we can build any level of nesting and reuse whole pieces of UI without the need for support in several places.

It often happens that on one page there are several fragments that should change their state when changing some general data on the page. To do this, they have a global (NativeJS) event bus through which they can communicate and respond to changes.

Shared Services

In the microservice architecture, central services inevitably appear, data from which everyone else needs. For example, a localization service that stores translations. If each microservice individually starts climbing for this data to the server, we will get just a shaft of requests during initialization.

To solve this problem, we have developed implementations of NativeJS services that provide access to such data. This made it possible not to make unnecessary requests and cache data. In some cases, even output such data to a page in HTML in advance to completely get rid of requests.

In addition, wrappers were developed over our services for different frameworks in order to make their use very natural (DI, fixed interface).

Pros of front-end microservices

The most important thing that we get from dividing a monolith into fragments is the ability to select technologies for each team individually and transparent dependency management. But in addition, it gives the following:

  • very clearly divided areas of responsibility;
  • independent issuance: each fragment may have its own release cycle;
  • increasing the stability of the solution as a whole, since the issuance of individual fragments does not affect others;
  • the ability to easily roll back features, roll them out to an audience partially;
  • the fragment is easily placed in the head of each developer, which leads to realinterchangeability of team members; in addition, each front-end can better understand all the intricacies of interacting with the corresponding back-end.

A solution with a microservices front-end looks good. Indeed, now each fragment (microservice) can decide for itself how to deploy: whether you just need nginx to distribute statics, a full-fledged middleware for aggregating requests to backs or support websockets, or some other specifics in the form of a binary data transfer protocol inside http. In addition, fragments can choose their own assembly methods, optimization methods, and more.

Cons of front-end microservices

  • The interaction between fragments cannot be ensured by standard methods (DI, for example).
  • What to do with shared dependencies? After all, the size of the application will grow, if they are not taken out of fragments.
  • Anyway, someone alone should be responsible for routing in the final application.
  • What to do if one of the fragments is inaccessible / cannot be drawn.
  • It is unclear what to do with the fact that different microservices can be on different domains.

Conclusion

Our experience with this approach has proven its viability. The output speed of features in production has increased significantly. The number of implicit dependencies between parts of the interface was reduced to almost zero. We got a consistent UI. Now you can safely test features without involving a large number of people.