An unexpected journey: The tale of the backend architecture at Alan

Photo by Raphael Koh on Unsplash

Alan is now 7 years old, and it’s a good age to take a step back on our technical journey. We would like to showcase the journey of how our backend architecture evolved during those years. And especially, why it evolved that way. There are no bad or good decisions, only different contexts.

The beginnings

Alan started in 2016 as a French company addressing the French health insurance market. Health insurance is very country specific, and focusing on only one country was a better way to assess our value proposition as a company. Remember we were a startup in a sector that didn’t change much in the past decades.

As we liked boring tech , we started with a flask application connected to one database. It was called app (we also like boring names). Our goal was to see if our ideas were working, and we were iterating very fast. This implied we didn’t have, nor needed, clear guidelines on our code architecture. You can imagine that we had a lot of business code in our models and in our controllers.

First iteration of the architecture
First iteration of the directory structure

Organizing things

This worked for the first few years. We didn’t have many engineers, all of them working closely in the same office. It was before COVID.

Around 2020, as we were more and more sure our business model made actual sense, we started to add features on top of other ones. We started to organize our code base a bit better. We extracted business logic from controllers and models to a dedicated layer of functions. We called it … business_logic (did I mention we like boring explicit names?). This allowed us to centralize some key functionalities. Of course we couldn’t stop to reorganize our entire code base. But most new features were developed using this new architecture.

Second iteration of our directory structure

Oh, let’s open more countries!

As our value proposition showed great promises in France, we decided to test it in other countries: Belgium and Spain. As for France, we wanted to be able to quickly iterate, so we decided to start from scratch, or almost.

Back in 2016, we managed to get our product ready (and our insurance license) for France in less than 10 months. 5 years later in 2021, we wanted to achieve a faster velocity for Spain and Belgium: we had 8 months to open both countries. As we had limited time and were not sure if Spain and Belgium products would work, we decided not to refactor our existing French code base to make it country agnostic. It would have taken months we didn’t want to risk investing for nothing.

Nonetheless, we didn’t want to reinvent the wheel, especially on low-level and low value technical components: think “I want to send an email”, or “I want to connect using a login/password”.

We put all our French code into a dedicated directory we called … fr-api (boring right?). We created new flask applications using the same foundations for Spain and Belgium in es-api and be-api. And we put our low level shared code in shared.

Again to iterate faster, we decided to have a dedicated database each country, and each country had a different database structure: it allowed us to implement custom features faster. So each country had its own runtimes and its own database, totally separate from the others. This new code was started with the business_logic pattern we mentioned earlier.

Second iteration of our architecture
Third iteration of our directory structure

Maybe we should reuse some code?

Unexpectedly, it took more time than anticipated to get the insurance licenses in Spain and Belgium. This meant we had a bunch of engineers not sure on which direction to go. And when engineers are bored they refactor and abstract concepts.

We also had code duplication between Spain and Belgium. We started to share more code. Our first — and probably naive — approach was to use classes, inheritance and dependency injection. Each service had a base abstract class that was handling the common business logic, and we had one concrete class for each country. Then we built bigger services that composed multiple others, with dependency injection. But again, as it was too costly to refactor what was in France, it was only for the two new countries.

Fourth iteration of our directory structure

Sharing features is better!

All was well in all countries. For one day.

Each country team was not incentivized to share their business logic: they coded mostly in their concrete class. They had to move fast, not see how their feature could work in other contexts. The goal to reuse code was quickly lost.

Our approach was clearly not the right one. As France had ten times more engineers than Spain and Belgium, the gap between the code bases grew exponentially.

It was 2022 and we decided to invest more in health services. It was mostly self-contained features on mobile and web. All of those were developed for France. As opposed to insurance, those SaaS-like features were by nature cross countries. The main blocker was localizing content.

Spain and Belgium were indeed very interested in those features. Around that time we also acquired a company, Jour, for its mobile application. We wanted to integrate some part of Alan in it and the other way around.

We devised a mechanism that allowed us to share features across multiple mobile applications. This mechanism had to handle users coming from different countries: each country had a separate database, meaning also a different user base! We also wanted users who didn’t have an account in our insurance stack to be able to use them (Jour users).

This mechanism relied on a front-end integration and shared secrets to encode data in JWT tokens. I won’t go into details on the exact “how”. Just keep in mind that we coded the backend for those features once, coded the mobile once, but the backend part couldn’t access data from outside (meaning other features or other countries). All the data that those features needed had to transit encrypted via the front-end, meaning HTTP calls made by a mobile application.

If you think it looks like a service architecture with the dispatching occurring at the frontend level, you are not wrong.

Third iteration of our architecture
Fifth iteration of our directory structure

More ownership is even better

As you can already see, this was overly complicated for very little added value. It also resulted in strange bugs of data being updated at unexpected times. Indeed, as the data could be cached in the front-end we had no guarantee that the latest version would transit. Don’t get me wrong, there were many good reasons to go that way, one was that we still had three different databases and three (four) different applications.

As this new pattern had some drawbacks, we went back to the drawing board. But this time we took the time to align product and engineering. This allowed us to dedicate engineering time to make the groundwork needed for our vision. Funnily, this vision was already there when we used the abstract/concrete classes. 2 years prior.

This is our latest iteration on our architecture. We are moving towards a Modular Monolith.

The goals were always the same: how can we reuse features to be able to have them in different contexts/countries?

We also learned from our past experiences and pushed to have an organizational reorganization at the same time. This allowed us to use the Conway’s law in our advantage:

Any organization that designs a system will produce a design whose structure is a copy of the organization’s communication structure.

The modular monolithic structure splits the code base in components that are loosely coupled with a high cohesion. We made our organization the same. We gave more ownership to each of our departments. Each of them has a clear ownership on their components. For this vision to work, we had to do some prep before we could start benefiting from MoMo: we had to make sure that each country could access the data of any other country. Indeed, as each component interacts with pure python code with other components, we had to make sure that any country could run any component. Also each component has a dedicated database schema. This meant each country had to be able to access any part of our database. We split our efforts in two phases:

  • Merge our actual postgres databases to one central one. Each country started to be in its own database schema: fr for France, es for Spain, etc.
  • Make the Spanish, Belgium and French code our first components. Indeed they were not (yet) interacting with each other, but sometimes we like to clean up our code structure

After those two phases, we could, at last, build our first piece of code that was truly shared and seamlessly integrated with all countries. We could rejoice at last. Or could we?

Sixth iteration of our directory structure, simplified for visibility

Tomorrow

Even if this is a great story, the reality is slightly different. Indeed, we won’t stop everything to rewrite all our existing features in our modular monolith.

We can’t pause the company for 1 year. Only some new features are, and some old are refactored. As we also didn’t stop on the previous steps, for the same reasons, we have in fact four different architecture patterns in our code base. This is clearly a mess we will need to live for a few years (let’s be brutally honest: probably forever).

We think we have the “right” pattern this time. Understand “right” as “the one that suits our needs and context of today and hopefully tomorrow”. This implies that we are ok with dedicating engineering time to refactor some old part of the code for our modular monolith.

We also know that the longer we wait for similar features across countries the more costly it’ll become to merge them into a single component. That is something hard to live with for an engineer, but as our company is not yet profitable, we understand that we need to focus on our business needs first: any technical decision must accelerate the said business.

Conclusion

The most important thing we learned here is that when you have a hunch you should not deny it, and dig into it.

If we did that, we could have merged our databases way sooner, and we would have more features shared across countries. This would have saved precious time to our operations, care and sales teams: they would have had one product and one tool, instead of three products and as many tools as countries.

--

--