This is the second post in a multi-part series on how we’re splitting the monolith that powers the customer-facing backend written in django into smaller services. You can read part 1 here.
Monolith vs Microservices
The concept of Microservices have heavily influenced how applications are built in recent years due to success stories from Google, Netflix, Uber, Amazon, Ebay etc. Microservices have become the de facto standard architecture for building large, scalable web applications. With all the hype around microservices, it might be tempting to jump into microservices architecture right from the start. After all, “monolith” has been named the number one enemy of scale both in terms of development complexity as well as the number of teams working on the software concurrently.
Why not just build with scalability, flexibility, autonomy, and all the benefits a microservice architecture has to offer from the start?
While the decision to go directly to microservices might be enticing, there are several reasons why it’s generally a bad idea for an early-stage startup.
Predicting the future is difficult
Microservices add complexity to the stack, there is no question about it. It increases your sources of failure (application code, inter-service communication, service mesh, kubernetes, scaling behavior etc) and generally requires a lot of time investment and skills to set up properly. At an early stage, there are too many unknowns and difficulties to predict how the product will evolve.
In an agile environment, you can’t predict the right splits for microservices right at the start. But putting every domain object into its own service comes with an unmanageable overhead, moving the complexity of the software out of the code and into the infrastructure and interfaces.
While our journey at TIER is about moving from a monolith to microservices, there are also many projects out there that successfully have many developers contribute to a monolithic code base. The most notable of these are the Linux Kernel and the Instagram backend, the latter of which incidentally is also written in python using django, just like our TIER backend.
Perfection is the enemy of done
When Richard Stallman started GNU in 1983, he set out to build a full Unix replacement, both user-space and kernel. While most people know the history of GNU/Linux, a lesser known part of that history is the story of the kernel that should have powered the GNU system, a microkernel called GNU HURD, whose development was publicly announced in 1991. Similar to Microservices, this microkernel would be small and communicate to other parts of the operating system, such as drivers, over a standardized interface.
When GNU HURD was built, it was written with the aim to surpass the rigid, monolithic Unix kernel architecture in function, security, and stability. The Linux Kernel which was started around the same time as GNU HURD was monolithic which was sometimes criticized, but the kernel’s simple architecture made it possible to quickly get onboarded and to hack it. The kernel attracted hackers and enthusiasts with openness, a free license coupled with the fact that it could be run on almost any existing device. The developer community around it grew very fast. Even though development continued on GNU Hurd, people were more interested in the more Linux Kernel until today, maybe because a monolith remains so much more “hackable” than a distributed approach.
The story of GNU hurd taught a valuable lesson. In your quest for the best architecture, it’s important to bear in mind that your users’ opinion matters. Getting something into the hands of your users (they may be developers too) quickly can be more beneficial than striving for perfection from the start.
Optimize for business objectives with team size in mind
The second example about a working monolith is about a small startup that allowed sharing squared images, 640 Pixels in wide and high. Before this small startup called Instagram was bought by Facebook, it had a team of only a few engineers. The core product was a django monolith using a hybrid of NoSQL and SQL database and a ton of open source tools. Instead of building their services and tools, they leveraged Amazon services and focused the efforts of their small team on solving problems that matter to the business the most. Instagram chose an architecture that will allow them maximize the efforts of their small team. At that stage, getting something into the hands of their customers quickly was all that mattered. Instead of moving away from the monolith, they have actually embraced this architecture and made it work for them. There are frequently no right or wrong answers in software. Many engineering decisions or architectures can be viewed as tradeoffs rather than good versus poor choices. What matters most of the time is optimizing for business goals with available capacity and resources.
Beware of the pre-emptive split
Microservices split business domains into small chunks to allow flexibility, stability and for rapid changes among many teams. The challenge is business domains are not always fully understood when starting out. At the beginning, your idea is still developing, you’re still discovering your customer, the market and the product are still evolving. It’s always hard to predict the right split for microservices and nearly impossible in an agile environment at an early stage. When you split your business domains without a full understanding of the product or the problem space, you risk creating a complex, distributed mess known as a “distributed monolith”. Distributed monoliths will bite you even in the long run and it’ll be more expensive to untangle than a monolith in a single system.
In software engineering, there are no single correct answers for all cases as each situation is different and unique. There are only trade-offs. Microservices architecture allows for quick, independent delivery of individual parts within a larger structure, making it difficult to do the wrong thing, according to those who argue that starting with microservices is a better choice. While this may be true in a few situations where you have a thorough understanding of your customers, business domains, and a large team capable of supporting distributed systems’ complexities, it is not true in the vast majority of cases. If your company is in its early stages, you’re developing an unproven product, and you have a small team with little experience with microservices, you should start with a monolith.