Brainly’s Approach to Service Hooks

Przemyslaw Nowak
Brainly Technology Blog
3 min readOct 20, 2022

--

Data fetching and data transformations are crucial parts of building applications. The services consumed change frequently, so your architecture needs to be flexible and easy to change. I wrote an article about creating abstraction over various technologies to decouple your code from specific libraries. However, it is not solving all of the problems we encountered at Brainly. In this article, I will present how Brainly approaches these problems and the solution we chose to move beyond them.

Photo by Conny Schneider on Unsplash

At Brainly, we have our framework built on top of NextJS called Brainiac. As part of Brainiac, we created an innovative way of building data services in React.

Common API

We used the pattern described in my previous article to build a common API and always transform results comes from the library (like Apollo or React Query) to a common type:

Thanks to that, we can be sure that it doesn’t matter if we use Apollo, React Query, custom fetch, SWR, or any other lib — we always use the same API.

This means we must build implementations for the technologies mentioned above, which is the trade-off we chose. The flexibility we gain is the ability to pick and choose which underlying abstraction over fetch is used for specific parts of our application, which is extremely valuable. It also makes our design decisions easier to change in the future. If we decide to replace React Query with SWR, we only need to change the service hook, and we know the blast radius of that change is limited to the hook implementation.

Data streams

Having this standard API is awesome for good encapsulation; additional benefits include the ability to create data streams that can be manipulated with Observables. Brainiac uses Zen Observable to develop and handle observable queries and transforms them as part of the React lifecycle.

To help visualize this, let’s imagine we are creating two services based on observables, one in Apollo and another in React Query:

Both services transform observables used in the React lifecycle. It looks great, but now let’s imagine that we would like to combine these services as a new service hook:

That’s pretty cool and easy! While there are alternatives to Observables, none of them are as robust at creating and manipulating data streams. Observables are also easy to test in isolation. Observables can also be consumed in any modern application framework. We can quickly write consumers for Observables that affect React state, Angular state, Vue state, or pure JS application state.

Let’s do another exercise — say that some data can be updated via web-sockets. With our architecture, all we need to do is write a layer on top of the web-socket connection:

This solution involves a lot of boilerplate code to get right. Brainly is highly focused on generators to automate boilerplate and ensure code quality. Using NX DevKit, we created generators for every layer of our architecture. Brainly engineers execute the following command:

$ nx run workspace-schematic service-generator --name=serviceA --serviceType=apollo

Then implement the service interaction using their chosen abstraction over fetch. Engineering is so simple with the Brainiac framework!

Summary

To create our service architecture, we used our experience and input from our engineers. This led us to leverage the power of Observables to provide a framework agnostic and well-tested service abstraction layer. We then leveraged NX DevKit to build a generator to ensure code quality. Our Brainiac framework will be open source soon; stay tuned!

--

--