Why a Monolith was the solution to the Microservices problem
Dynamic companies will have complex needs, and those needs can only be solved with solutions that can scale to fit. That's the line they use to sell microservices. (We use that line too, and we do stand by that line). See, this is true for the giants in the industry with structures that encapsulate a lot of diverse functionality built into a single codebase with no barriers between them. For them, the downsides of the monolith outweigh its benefits.
Microservices has surged in popularity in the past couple of years and has been touted as the end-all solution to all the problems arising from monoliths. Yet our collective experience told us that there is no one size fits all answer to your problems mostly, and microservices will bring their own set of issues. We still chose to work with a firm and help them adopt the microservices architecture early-on, which served them well for over a year, and, as we'll soon get into it, not so well after.
Microservices is a software architectural style that is service-oriented where server-side applications are built by combining many low-footprint, single-purpose network services. The touted benefits are reduced testing burden, improved modularity, better functional composition, development team autonomy, and environmental isolation.
Two years after deployment, instead of empowering them to speed up as it did the first year, they found themselves mired in increasing complexity. The promised advantages of this architecture became burdens. The defect rate exploded as their velocity plummeted. The team eventually found themselves unable to make headway, with two full-time developers spending most of their time just keeping the system functioning. They needed a change. Here we're talking about how our team took a closer look at their problem and worked on an approach that aligned well with the needs of their team and their product needs.
Why a Monolith worked for them
Monolithic architecture is the easiest to implement. The result will likely be a monolith if no architecture is enforced. A huge amount of functionality lives here in one service that is tested, deployed, then scaled as one unit. With Ruby on Rails, this becomes especially true. It lends itself well to building this structure thanks to the global availability of all code at an application level. Since it's easy to build and allows the team to move fast, in the beginning, to get their product in front of users, Monolithic architecture has its advantages.
Maintaining the whole codebase in a single place, then deploying the application to one place has many benefits. You only have to maintain one repository, and can easily search and find all functionality in a single folder. Also, you'll only be maintaining one test and deployment pipeline, which can avoid a lot of overhead depending on the complexity of your application. But these pipelines can become costly to customize, create, and maintain as it takes considerable work to ensure consistency across it all.
A compelling advantage of choosing the monolithic architecture over the microservice one is that you can directly call into different components, rather than having to communicate over web service APIs. Here you won't have to worry about API version management and backward compatibility or potentially laggy calls.
Moving from a microservice architecture to a monolith did help a lot, but there are trade-offs we were sure to warn them of:
In-memory caching becomes less effective.
With one service per destination, low traffic destinations only had a couple of processes. This kept their in-memory caches of control plane data hot. After shifting to the monolith, the cache was spread thinly across over 2500 processes making it more likely to be hit. We did use Redis, and it's like to combat this, but it becomes another point of scaling which we'd have to account for. Given the substantial operational benefits they were looking for, they decided to accept this loss of efficiency.
Fault isolation became difficult.
In a monolithic structure, if a bug is introduced in one destination that affects a particular service, the service will crash for every other destination too. To combat this, we introduced comprehensive automated testing. However, tests can only get you so far. Currently, we are working on a more robust method to prevent one destination from taking down the entire service while still holding on to the monolithic structure. If you'd like to know more about how we're accomplishing this, you can speak to our engineers here.
Updating the version of dependency can break many destinations.
Moving everything to one repo solved the dependency mess they were in, but it meant that if they wanted to use the newest version of a library, they'd have to potentially update every other destination to work with the newer version. But the simplicity of this approach was worth the trade-off for them. And with a comprehensive automated test suite, they can fast see what breaks with an updated dependency version.
The initial microservice architecture worked for a time, solving the immediate performance issues in their pipeline by isolating the destinations from each other. However, they weren't set up to scale, and they lacked the proper tooling for testing then deploying the microservices when bulk updates became necessary. As a result, their productivity declined fast.
Embracing a monolithic structure, while significantly increasing developer productivity allowed them to rid their pipeline of operational issues. This transition wasn't made lightly though and they knew that there were things they had to be aware of if it was going to work. They needed a rock-solid testing suite to put everything into one repo. Without this, they would've been in the same place as when they first decided to break it apart. Constantly failing tests hurt their productivity in the past, and they didn't want that happening again.
When deciding between a monolith or microservices, there are various factors to consider. In some parts of your infrastructure, microservices might work well, but your server-side destinations could be a great example of how this architecture du jour can actually hurt your productivity and performance. It might turn out that a monolithic structure might actually be the solution for you.