By: Erik Landerholm
Ahh… the question we all struggle with: Microservices or Monolith? OK, not really. More than likely, wherever you work you will find examples of both. At TrueCar when we started our journey out of our data center and into AWS, we didn’t want to lift and shift… well… anything. We needed to rewrite the entire application and supporting code in order to take advantage of modern dev tools and processes. Our ultimate goal was to get out of our data centers into AWS and become an Agile development team. Ultimately, CI/CD (which is another series of blog posts we’ll be releasing shortly) became the goal everyone could rally around.
We’re going to break this topic into two parts: why and how. There are already plenty of blog posts generally detailing why you might choose Monolithic vs. SOA vs. Microservices. We want to speak more specifically about why a Monolithic API back end written in Rails is right for us and “how” we avoided or minimized some of the inherent disadvantages of a Monolith (leaky abstractions, etc.). To be clear, the Auto Buying Platform (ABP) API Monolith isn’t the only application or code base in the company. But it does power everything customers interact with on TrueCar.com, our 700+ partner sites, all of our dealer tools, and our mobile offerings.
The way the legacy ABP was “architected” made that impossible. We needed to completely rewrite everything that powered TrueCar.com and our 700+ partner sites, dealership products, and mobile offerings, and move it to AWS and implement an Agile development process. The details on why it was necessary are outside of the scope of this blog post. We’ll focus on how and why we used a Monolithic back end, built with Ruby on Rails to make the process as painless as possible.
Whether you’re building an application from scratch or rewriting a legacy application, you’ll need to think about and ultimately decide on an architecture driven by your philosophy, which in turn is driven by the mechanics of the application. Most web applications fall into one of two camps (I’m generalizing a lot right now):
a) Large amounts of traffic with a relatively simple data model.
b) Less traffic and a relatively complicated data model.
TrueCar.com is more b than a. Our database is going to be our bottleneck even at 700+ employees and ~$400M in revenue.
- Our marketplace is efficient but complicated.
- Our back-end development team numbers in the dozens, not hundreds or thousands.
- Car data is notoriously messy and deep, with many levels of relationships, lending itself perfectly to a RDBMS.
- We have millions of users and multiple millions of UVs a month, but not billions.
Back when I started at TrueCar in 2014, our development process was notoriously slow, with a single deployment every eight days. Fast-forward five years and we’re doing more than a hundred deploys each week, with 98% code coverage on our new ABP back end, built with Rails. We knew we’d be able to make our APIs perform at an acceptable level; what we needed to optimize for was developer speed!
Advantages of a Monolith
Before we talk specifically about how we structured our Monolith to minimize some of the disadvantages of a Monolithic code base, let’s talk about its advantages. One of the biggest (and most underappreciated, until you don’t have them anymore) wins in a Monolithic back end that uses an RDBMS is the ability to use JOINS. Imagine you have a user model and you would like to retrieve all of the vehicles they’ve looked at. With a Monolithic database and application, this is accomplished by one or more JOINS from Users -> Vehicles (in reality, it’s more complicated than that) instead of authenticating and authorizing and calling multiple services.
You could think of JOINS as your API between services. Obviously, there are limitations to this kind of thinking, in the same way that you can’t make an unlimited amount of calls to different service APIs in a Microservice architecture. Those network calls, lookups, JSON (probably would have to move away from JSON for communicating between services) serialization, etc., for multiple services aren’t free.
The ability to authenticate and authorize, make any API calls, and gather all the required data and organize it for display inside a single service (ABP) means we can choose the language and framework that gives us the highest developer productivity, instead of needing to optimize for runtime speed. Having the fastest-moving developers is a massive competitive advantage!
Besides the two big advantages listed above, there are many more when all of your code and functionality is in one place:
- No deployment dependencies (dependency hell!): Which version of this service is compatible with that service? Shared libraries or not? How do I find out what services are available and what they do exactly? This is much easier in a well-documented and organized Monolith — a majestic Monolith if you will.
- Error tracing: Entire transactions are logged from one place to one place.
- Testing: We have 98% test coverage on the Monolith, and full app integration testing is done from our FE Monolith. Having more than one Monolith isn’t an oxymoron!
- No silos: It’s very easy for developers to work on different parts of the app, as it’s all architected in a common way, using the same tools and with no distributed computing knowledge needed.
- Shared code: No shared libraries or “no knowledge” transactions where the entire scope needed for the service to operate is sent along with each request.
- Cross-cutting concerns: We have a lot of them; you probably do too! Spending a lot of time defining services that don’t bleed into each other is time you could spend building things that help customers!
All of the above advantages can be mitigated or even realized with Microservices, but it comes at a cost. That cost is more complicated development, testing, monitoring, and deployment. Microservices architecture is like XML (or violence): use it sparingly and only when truly needed!
Disadvantages of a Monolith
Nothing is free in life, or perfect. Every decision has pluses and minuses and side effects, some of which are difficult or impossible to predict. A Monolithic software architecture is no different. Here are some of the disadvantages that we’ll discuss in our “how” post:
- Performance: This is probably the most loaded of all “disadvantages,” as it’s also an advantage. We optimize our database, cache a lot, use Sidekiq to make everything we can async, and ultimately pass messages outside the monolith with something we call Wormhole (Enterprise Service Bus built with Kinesis).
- Agility: Also an advantage of the Monolith, but depending on team size, number of services, and scale, this can become a disadvantage. Your development process can greatly mitigate this issue.
- Leakiness: As you write more and more functionality into your Monolithic code base, it’s inevitable that some of it won’t stay in its lane. Abstractions and functionality will leak into other parts of the Monolith. There are ways to mitigate this, and we’ll go into depth about how we deal with this issue. The corresponding issue in a Microservices architecture is that you will often duplicate functionality and code in services.
- Testing: Microservices allow you to isolate your concerns inside various code bases. This has advantages when doing unit testing, but the complication comes during integration testing.
- Deployments: Like testing, deploying a single service is simpler and faster than a Monolith consisting of many services, but the devil is in the coordination when you have many services. This can be made easier in a Monolith with good service/functionality isolation.
Hopefully, this gives you some insight into our thinking on why we decided to pursue a Monolithic back-end architecture for our rewrite, as opposed to Microservices. Stay tuned for our post on how we did it. I think it will give you some really useful insight into how to manage your own Magnificent Monolith™ (trademarked, as someone already took Majestic 😉).
Stay tuned for the next installment, all about the “How!” If you liked this one, you will love our upcoming posts about Spacepods (our deployment platform), CI/CD, Armatron (our ETL platform), Gluestick (our FE framework, built with React), Wormhole (our ESB), and more!