Gympass front-end architecture redesign

Kaic Bastidas
Wellhub Tech Team (formerly Gympass)
6 min readMar 24, 2020

Last year, in December we had the opportunity to revisit our current front-end architecture. In this article I will try to explain what we did exactly.

Let’s face it, working on the platform means not working on features and no actual changes to the end user. As most companies, we balance between product and engineering demands. We had just released our gym details page and with no upcoming product demands there was an opportunity to take some time and work on a new front-end architecture.

Before explaining everything new that we created, we should explain why we needed an architectural redesign. Our current stack consists of a Server-side rendered React App, which is developed all in-house with a legacy code that is really confusing for almost anyone that tries to change something on it. Each front-end micro-service is a repository with this stack and k8s ingress with nginx takes care of redirecting to the correct service.

Current stack example

We have 3 main problems with our current stack:

1. Creating a new service is really hard.

We did not have a scaffolding tool available (little spoiler), so any time we wanted to create a new service we cloned one of the repositories, usually the one with the most updates to our core functionalities (which is also a problem that we will soon talk about). But that’s not all! After cloning the repository we need to remove everything that we will not need from it, which takes a lot of time when you don’t really know what you need or not.

Let’s say we are done with the repository, removed everything that we didn’t want and created our new amazing page. Now what? Creating the deploy pipeline of our service was all manually, which we would create with the help of our SRE team. This also takes time since they are pretty busy with Site Reliability problems, meaning that we needed to create a Jira card and wait until they were able to work on it.

2. Multiple BFF’s with replicated logic.

Each front-end micro-service has a dedicated BFF (back-end for front-end). This BFF is integrated with front-end service, the same express server that renders our React to html to SSR. It also has the /api routes.

The BFF is needed since most of our API’s hosts only exist inside our k8s containers. This would be fine if we didn’t have a lot of routes that do the exact same thing on EVERY BFF, like getting translations, user or country data, for example.

All the API’s routes consume the same service

This ties really well with our third main problem.

3. Propagate changes to the base code.

Our stack is created all in-house. While this gives us a lot of liberty, we also have some problems in the way it was done. The code is all inside the repositories and not on separated packages, this means that if we make a change on any of the services, we need to propagate it to the remaining ones. For example, if we change something on /users we need to propagate it manually (creating an PR) on all the other repositories.

The consequence of that is that we now have a lot of differences between what was supposed to be the same code stack, not only features but some security upgrades too, and that should not happen.

Other than these 3 main problems, we also have a lot of legacy code that has a lot of code smell and bad practices. We also have a HUGE boilerplate to create anything, be it new page routes or API routes on our integrated BFF. Having the BFF and the SSR engine on the same server also means that we have coupled them and there’s a single deploy pipeline for them both.

Also, having these problems really discourages us as engineers on creating new front-end services and that made our current repositories turn into little monolith applications with multiple contexts inside of it.

And since we did not have the habit of documenting things, we have a pretty bad documentation of the current stack.

The solution

Let’s solve our problems one by one, shall we? The first problem is that it is very difficult to create a new service. To solve that our SRE creates an inside tool that we call Josie.

Josie takes a template as a parameter and generates a new service from scratch, creating production and homologation environments with just a simple command.

That’s it. We now have a simple app running on production with a single command. There are still some config’s that we have to do manually, but this CLI simplified our life a lot.

Now all we had to do was to create the template. We thought about it and reached the conclusion that we can be more productive using an existing framework, with an dedicated team working solely on the SSR engine, then trying to give maintenance to an in-house engine WHILE creating new user experiences (we may change this vision on the future, when our team grows and our platform gets more robust and that’s absolutely normal). That’s why we’ve chosen Next.js.

Gympass + Next.js = ❤

We proceeded to create the template with everything that we thought would be necessary to develop our future features. We then got to the next problem: what if we wanted to change something to all the services that use this template?

To circumvent this problem we created an internal package to be used on the template, with everything that we thought was core for our application. Configurations, helpers, components, you name it, we have it all inside this package.

When we change something, all you need to do is bump the package version on your package.json and get the latest code. We reached this final infrastructure:

Final service structure

We still have the last problem to solve, which is multiple BFF’s with the same API’s. This was probably the biggest change in our developer mindset. To power our current native app (iOS and Android, written in react-native), we developed a GraphQL BFF which was being used only for the app. We were not using it on the web because as explained before, changing the core on our legacy code base was pretty hard.

We took the opportunity to create this interface with it for the front-end applications too, allowing us to keep the same logic in a single, concise, place. Now features that exist both on the app and the web can use the same queries to build the interfaces. This is a dream come true.

Our example, following this new structure would look like this:

Our legacy application uses Redux to manage the state of the application, now we only rely on Apollo to do that (that’s why this was a big mindset change).

Although this change is pretty nice, we didn’t want to force it, that’s why we made the Apollo provider optional through a higher order component, which also comes from our core package. This is an example of the final result:

With this current stack we also solved the other problems that we had, with a new and better code and less to none boilerplates.

Now we are done. New stack, new infrastructure and the most important thing, new culture mindset. Creating a new service is now cheap and easy, meaning that we can and should give more shots on trying out new user features.

This was the first time that I was able to participate in creating something that will be used by other developers and it was pretty rewarding.

Now, as a shameless plug, our template uses our design system! You can check it out here.

--

--