From a Monolith to a Distributed Architecture, Without Rewriting the Product

Impact

~10 microservices, zero rewrite

A monolith was the right call at first, until modules with very different needs started competing for the same infrastructure. I introduced a providers layer to migrate responsibilities into microservices without rewriting the product or stopping development, focused on fault isolation: a secondary service going down should not take the core with it.

Software Developer

When a product is just starting out, a monolith is often one of the best decisions you can make: it lets you move fast, keep costs low, and avoid the operational complexity of multiple services, databases, and deployments from day one. That is exactly what we had: a backend that had grown inside a single application, with multiple modules sharing the same database engine. It worked well until the product grew, and those modules, each with very different consumption patterns, started competing for the same infrastructure resources. The problem was no longer the monolith itself: it was that responsibilities with very different needs were sharing the exact same infrastructure.

Finding what could be separated

Instead of proposing a full rewrite, we analyzed the system module by module, looking for functionality with a responsibility clear enough to isolate, especially anything that did not need constant access to the product's most critical data. The goal was to keep only what truly belonged to the core in the central database, sensitive data and the product's fundamental entities, and progressively move everything else into independent services. The strategy was deliberately incremental: we did not want to halt development for months to build a new architecture. We wanted to change the architecture while the product kept evolving.

A providers layer

To make the transition without coupling the rest of the application to a specific implementation, we introduced a new layer inside the traditional controller/model pattern: providers. They worked as an abstraction between the application and the implementation of a given piece of functionality, so a responsibility could initially run inside the monolith and later be replaced by an implementation living behind an independent API, without the main application needing to know where it lived. This was key to migrating progressively: the central API started talking to different providers, and those, in turn, to independent services, until we had an architecture made of smaller services, each with its own infrastructure and, where it made sense, its own database.

The goal was not to have more microservices

The motivation was not adopting microservices as an architectural trend. It was reducing coupling and isolating responsibilities: if a secondary feature had a problem, we did not want it to be able to compromise the product's core. It is the same principle Netflix popularized: if the service that fetches a movie's description goes down, that should not stop you from playing the movie. If a secondary service fails, that service should fail, not the whole product. This separation also let certain services evolve, deploy, and get debugged independently.

The services also needed to talk to each other

As the architecture grew, some microservices needed information that belonged to other contexts. The simplest solution would have been letting each service connect directly to the others' databases. We decided not to: in several cases we introduced services that worked as bridges, exposing over an API the information another service needed without giving it direct access to a database that was not its own. A service talked to the bridge, and the bridge handled the communication with the corresponding service or with the central API. This kept a clear boundary between responsibilities and kept the physical separation of databases from creating a new kind of coupling.

A migration that evolved with the product

The architecture ended up moving from a central API with multiple modules to a network of specialized services: some fully independent, others needing to talk to each other, and in some cases services acting as intermediaries to keep those dependencies under control. We ended up with around a dozen microservices, but the number was never the goal: each service existed because there was a responsibility worth separating. We also did not abandon the monolith overnight. Both architectures coexisted for a while: the core stayed centralized while we progressively extracted the responsibilities that could be isolated, which let us keep shipping new features while gradually reducing the pressure on the central infrastructure.

Result

The new architecture distributed the load that used to fall on a single database and separated responsibilities with different infrastructure needs. But the most important benefit was different: the system stopped having a single point of failure for all of its functionality. A secondary service could run into problems without compromising the core's availability, issues were easier to localize, and services could recover independently. The experience also reinforced something I still keep in mind when designing systems: there is no architecture that is correct forever. The monolith was the right call when we needed speed and simplicity. Microservices started making sense once the product's size meant some responsibilities needed to evolve independently. The key was not choosing an architecture from day one, but recognizing when the one we had had stopped being the right tool, and finding a way to evolve it without stopping the product.