Sitemap
Doctolib

Since 2013, Doctolib has been transforming healthcare for thousands of health professionals and millions of patients across Europe.

Scaling: Making Doctolib Small Again

--

At Doctolib, we’re living the dream of a small startup: we’re getting bigger.

By bigger I mean:

  • 100s of developers,
  • 1000s of employees,
  • 100000s of doctors,
  • 1000000s of lines of code,
  • 10000000s of patients,
  • 100000…00s of visits on the website,
  • And it’s only the beginning!

That’s very nice, but growing in size brings complexity in everything. Our goal now is to scale, which means to accommodate complexity and not collapse under our own weight.

What would be fantastic is if we could manage all this bigness as easily as we managed the small startup. This has become our new dream at the tech.

A while ago we presented our boring architecture striving for simplicity at all levels. We still are, and will always be, a user-first company building the most useful — not ‘fancy’ — products for users. This is why we’ll always strive for simplicity.

But as we grow, the Tech and architecture to support this product is becoming more challenging than ever before! We’ve launched a Tech Scaling Program and we’re looking for people to work on it with us.

Here is our plan to make our tech scale to help us transform healthcare. But first, let’s see what it means to “scale” the tech of a startup. And hint: it’s not about microservices.

Scaling means becoming small again

At the beginning of any project, be it Doctolib or any other company, there is little code:

Little code means that everyone can have a mental map of the codebase. This means in turn that developers can anticipate the impacts of changing the code. It makes it easy to ship features fast and without bugs and keep code design consistent. Put another way, everyone feels comfortable and knows what they’re doing.

This is the happy technical environment of a small successful startup.

Getting bigger

When we add time, features and staff, the codebase gets bigger. And it gets bigger faster and faster.

If we only focus on adding one feature at a time, without taking a step back to keep control of the codebase, the mass of code becomes overwhelming:

If the codebase is so large that no one can have a mental map of it, then we enter a dangerous cycle: developers have trouble understanding all the code and can’t predict all the impacts when they change the code. This creates bugs. Some of them are caught by the tests, and some slip through. This makes it longer to deliver features, and harder to keep the product stable.

Also when the codebase gets complex, it’s harder for a developer to understand the intentions in the design of the existing code. This makes it harder to keep the design consistent. Which in turn makes it harder for the next developer to understand the intention of the design. Until chaos has replaced design in the code.

In summary, the more code, the more complexity, and the harder it is to ship features, keep the quality of the product and manage the tests. This is not where anyone wants to get.

Keeping the code under control

If we want to keep our codebase manageable, does it mean that we have to throw away features, remove tests, fire people, and be content with influencing healthcare instead of transforming healthcare?

There is another way. Instead of having one mass of Doctolib code that gets out of control, we want to have many small manageable entities that form the entire Doctolib:

Having small entities means that the codebase is chunked up into logical entities that have the following two properties:

  • The role of each entity is precisely defined
  • The interactions with other entities are precisely defined

If we manage to achieve that, then we’d be back to the environment of a small startup again and continue growing at the same time. That is our new dream.

Indeed, any entity would fit in the head of a developer, and the defined interactions with the other entities act as a proxy for them. So we don’t have to have more than one part of the system in mind at a given time.

That doesn’t mean that a developer working on one entity doesn’t know the other parts of the system. We have an internal open source culture at Doctolib and we don’t want it to stop.

The goal is rather to allow anyone to reason about each part of the system at a time.

In summary, for us scaling means breaking up the code to go back to the technical environment of a small startup and continue growing at the same time.

Now that we’ve defined what scaling means, how do we achieve it?

How to split the codebase

Before splitting up the code into smaller components, let’s define what we mean by a “component”.

This question has two aspects: how is a component implemented, and what scope a component has.

There are many ways to implement components, and one that could come to mind is to represent them as microservices.

This is not the option we’re choosing today at Doctolib.

Microservices are a powerful technology that brings a lot of flexibility. For example they allow to develop several parts of the system in various languages, to adjust the infrastructure to the various needs of the parts of the system, and much more.

But microservices come at a cost. They bring complexity in communication (they replace some function calls with http requests), in error handling (a function is never down, but a service can be), in deployment (changing the code of several services at the same time needs special care), and more.

Today we don’t need the flexibility of microservices. Maybe one day we will, but our healthcare oriented monolith sustained the Covid this year and this was a bigger charge that we could ever imagine.

Even if we could draw some benefits from microservices, today they wouldn’t justify the costs they bring. Microservices are trendy and exciting, but we value the boring architecture.

Rails engines

Our choice is then to keep a monolith, but to tidy it up well. Some call that a modular monolith. Others go as far as a majestic monolith.

A natural choice for components in a Ruby on Rails codebase is Rails engines, and this is the option we’ve chosen at Doctolib.

There is a whole lot of technical documentation on Rails engines, but in a nutshell a Rails engine is a directory containing the same layout as a Ruby on Rails application. It’s like a small app within the app.

To illustrate, this is the typical layout of a Rails application:

When using engines, we have a new directory at the root of the project, that contains replications of the main layout:

Engines are sub-directories looking like applications within the main application. Each engine has its own models, views, controllers, routes, tests, etc. They can also have their own frontend code.

What helped us create engines is to write a Rails plugin script to generate an engine with a given layout. A developer who wants to create an engine just launches this script, and then moves the code from the main application over to the engine, directory by directory.

This makes it easy to create engines and ensure all engines have a common layout.

The scope of an engine

Now that we’ve defined how we represent components in the codebase, the next question that comes up is: what should those components do?

Our purpose here is to split the application into components that have a well defined role. But how big should that role be?

There is not one good answer to this question, but the guideline we like most at Doctolib is one we’ve heard from Philip Müller from Shopify: the scope of an engine could be the one of a small successful startup.

We like this guideline for two reasons. The first one is that it matches well our vision of scaling as going back to the environment of a small startup.

The second reason becomes visible if we make the thought experiment that the code of an engine was not ours, but rather it was made by a small startup to which we outsourced some work. We can then imagine that our interactions with this code would be very clearly defined, which is what we need for scaling.

If an engine is like a small startup, it is one that is set up for success, as it has access to the ecosystem of the company, for example a mature design system, our directory of doctors, etc.

In our healthcare oriented codebase, we have extracted a dozen of engines so far. For example we have an instant_messaging engine that contains our chat between healthcare professionals. We also have a customer_identity engine that handles the verification of identity and right to practice that we need from doctors to let them use Doctolib.

Dependencies between engines

As we have defined them so far, engines are just directories. In particular, they are free to communicate with each other and with the main application.

Our plan now is to work on dependencies inter-engines and between engines and the main application.

What we expect to find when we identify our dependencies would look like software entropy:

And the long term plan is to straighten them out in a way that makes sense for us:

Next steps

We’re at the beginning of this tech scaling project now, and the next steps we see are:

  • Identify the dependencies
  • Fix the dependencies that don’t make sense
  • Make sure we don’t add new dependencies that don’t makes sense

Note that in this plan we don’t look at the internal dependencies of the engines, nor the internal dependencies of the main application. We focus on the dependencies inter-engines and between engines and the main application.

This approach allows us to tackle the most important dependencies first, and also provides an attainable goal. Since the code of an engine is not too big, tackling all the dependencies of a given engine seems like an achievable and exciting goal.

This perspective also gives a strong incentive to create engines, which in turn should improve the overall state of the codebase.

We’ve benchmarked many ways to identify dependencies on our backend Ruby on Rails code, and the option we retained is to use Shopify’s packwerk.

Packwerk does many things, such as dependency declarations, but so far we only use it to list the dependencies between engines and main application, with the command packwerk update-deprecations.

On the frontend side we didn’t find an appropriate tool to list the dependencies between engines and main application. So we wrote a script that parses the imports of JS files, and that produces the inbound and outbound dependencies of a given engine.

We’re now in the process of analysing those dependencies, and we are starting to observe interesting findings.

This is the beginning of a super exciting project at Doctolib. We’ll write more about it when we have more findings to share.

If scaling the tech by designing for modularity is the kind of problem you enjoy solving, come and discover the next findings that will scale our tech to transform healthcare. Apply to our jobs and be sure to mention you have an interest in the Tech Scaling Program.

I also encourage you to subscribe to our tech newsletter. Every Tuesday you’ll get 12 selected resources on React, Rails, software engineering best practices, and nerd stuff. This is my personal go-to resource on those topics.

--

--

Doctolib
Doctolib

Published in Doctolib

Since 2013, Doctolib has been transforming healthcare for thousands of health professionals and millions of patients across Europe.

Jonathan Boccara
Jonathan Boccara

Responses (1)