A long time ago in a galaxy far, far away, programmers used to build applications that were as fast as possible with the extremely expensive hardware that was available—no cloud computing, no multi-cores, just one CPU and some lines of code trying to run quickly enough to provide a decent user experience. And it worked… for a while. Though not every piece of code that got churned out was truly optimal, hardware was evolving so rapidly that most problems could be fixed simply by adding more processing power. Moore’s Law stated that processing power should double—and the cost of computers should halve—every two years, and for a while that held true. The result was that hardware got faster and faster and faster, consistently coming to the rescue of programs that might have otherwise run too slowly.
But all good things must come to an end eventually. When Moore’s law stopped being a reliable predictor of hardware improvements people began turning to multi-core hardware, and scalability became a major concern for coders across the world and businesses they supported. Ultimately, with the rise of cloud computing a new (well, not really that new…) way of building applications became dominant—the cloud was supposed to be the foundation for infinitely scalable applications that offered a stable and reliable user experience. But anyone who spends a lot of time using or working with software can tell you that that’s not always the case. For every smoothly-running cloud app that can process transactions quickly and efficiently, there’s another one that’s slow and painful to use.
So, what gives? Why has the cloud seemingly failed to deliver on its promise of making processing power irrelevant and scalability a breeze? To answer that, we first have to talk about microservices.
The Rise of Microservices
Microservices, for the uninitiated, are essentially loosely coupled, independently deployable apps that are organized around specific business functions. Microservice architecture allows small teams to rapidly and easily deliver, test, and maintain small segments of a much larger system without risking the stability of the system in its entirety. In more traditional monolithic applications, all of your functionality resides in one code base, and changes to any piece of the application have the potential to impact functionality elsewhere—meaning that things often move slowly and require a lot of oversight. With microservice architecture, you essentially avoid these issues, resulting in a number of distinct benefits.
- It’s easier to build and maintain apps: Simplicity is the key principle of microservices, which means that applications are broken down into a group of discrete chunks specifically designed to perform a specific job. This logical island is self-contained, so the architecture ensures it has all it needs to perform its job; since each chunk is only designed to support only one piece of the business, it’s easy to use the best technology in every microservice. Also, as every microservice is basically a self-contained piece of code, if a process is hard loaded in a specific microservice it won’t affect the performance of other microservices (at least not in terms of hardware).
- You can organize functionality around specific business needs: Microservice architectures should be designed to reflect the realities of a specific business domain, such that the entire system design is fundamentally business-driven. Little pieces of code support every individual part of the business, theoretically enabling faster responses to business changes, new rules, or completely new business areas.
- Speed, productivity, and scalability are easier than ever: The whole idea behind microservices is to be responsive to your business—breaking down the application into chunks of self-contained code makes this easier, because changing one component shouldn't impact any others, since the whole whole system is composed of multiple separate chunks of code. Crucially, this allows for real scalability. Why? Because if there’s a strenuous load on the system, you can just add more copies of the overloaded component to balance the load and solve any potential malfunctions.
What’s the Catch?!
In theory, this is a great way to get the most of your cloud deployment. Microservices power easy deployment for scalable applications that take advantage of having essentially limitless processing capacity in the cloud—so what’s the catch?
Scalability. Now, we know what you’re thinking—the whole point of microservice architecture is to improve scalability, so why is scalability an issue? Simply put, scalability becomes a trap when it’s being used to solve performance issues that stem from poor application design. Moore’s law taught programmers to believe that when things weren’t running well, more processing power would eventually save the day. But that’s not really the case anymore: piling up instances after instances on a component that’s performing transactions too slowly isn’t going to speed it up. Why? Because the issue isn’t a lack of computing power, it’s the code itself. And though one problematic microservice won’t impact any other services on an architectural level, those discrete programs do have to work together to perform larger workflows. That means that not only will the user have to wait for this slow microservice to finish running its transaction, there will also be a bunch of other live microservice instances sitting idle and unused.
If this sounds like it runs contrary to the way that people talk about scalability, remember this: scaling works for managing loads. If a ton of people are requesting a specific service at the same time, you can scale up by running more instances to process those transactions all at once—but those extra instances don’t speed up each individual process. The only way you can make that happen is by writing an efficient program in the first place.
Let’s look at a real life example as an elucidation of this point: recently, I was working with a new system that was built using microservice architecture. One of its goals was to keep queries flat, which it sought to accomplish by doing all of the filtering, joining, and data ordering on the application side. The idea here was that application components can be scaled up easily, and database functions can’t—but there’s a crucial flaw in this logic: databases are much, much faster when it comes to fetching data than application-side functions are. Can you guess what that result was? That’s right—extremely slow transactions, with a high rate of bottlenecks. In this case, adding more instances to power the application-side code wasn’t helpful, because this code is slower than normal database functionality by its very nature. The goal was to prevent the database from becoming overloaded; they succeeded in that goal, but only at the expense of speed.
How to Create Truly Scalable Microservices
In the scenario we sketched out above, it took several code refactors to make the individual transactions perform well. Obviously, if your goal is to build and maintain apps more rapidly, lengthy rework is the last thing you want. This means that even when it comes to microservices, you really want to do it right the first time by building something that’s designed to run efficiently. If you can’t make that happen, you’re potentially doomed to slow run times no matter how much processing power you throw at a particular application.
So what’s the key to making that happen? Balance. You need to design applications in such a way that you can exploit strengths on every layer, and thereby address every task in the most performance-optimal way. Scaling can’t be used as a tool in your code or your design for an individual component, because what essentially doing is papering over inefficiencies that will come back to haunt you later. Especially if you’re working the principles of microservice architecture, the right design is the one that will perform just as efficiently with one instance running as with ten.
Keep in mind, to make microservices work, you need to hold true to the philosophy behind them. That means no program-coupling just to try and make things faster—because this jeopardizes that benefits that come from the discrete chunks of code to begin with. If you can keep all that in mind, you can build applications that scale seamlessly when there’s more load to deal with than usual, meaning that you can conduct critical business functions with confidence that your infrastructure is going to support you. Whether you’re trying to beef up an internal accounting tool so that it will maintain low latency during crunch time, or you’re building out client-facing search functionality for a database of products and you expect a lot of seasonality, if you start with microservices that are built out efficiently you can create something that’s truly scalable and will support changing load volumes across different business functions.
There’s nothing more exciting than new, game-changing technology—but as technologies evolve we have to evolve with them. The cloud promises virtually infinite scalability, but with the rise of microservices we’re learning that that’s only true if you take an extremely careful and conscientious approach to these tiny pieces of code. If you can’t deal with any performance issue in a small scale deployment, those performance issues aren’t going to go away in larger scale environments with more processing power—meaning that your operational efficiency, and thus your health as a business, is potentially at risk. This might sound like a lot to grapple with, and in a way it is, but if you can use the right approach, with the right tools and techniques, it can be a game-changer for the performance of applications, and you can avoid the scalability trap.