We’ve all been there. You start your pretty new [insert language here] project with a vow to do things right. You carefully discuss the project structure, you agree on what the database schema will look like, and your unit test coverage is at a staggering 99.8%! “It’s perfect,” you think as you crack that well-deserved beer and watch bits flow happily through your brand new service.
Fast forward six months, and you’re presented with a completely different scene. The latest set of features has wreaked havoc on your precious project. You have files with 500+ lines of code, your coverage has sunk to a barely acceptable 70%, and your list of complaints with every architectural decision you’ve made is growing by the minute. What an idiot past you was! What happened, and how do we fix it? When we develop new software, we are often afforded the luxury of solving a single problem or at least a series of related problems. As the business grows, the problems we solve become subtly more complex and diverge further and further from the project we were trying to solve originally. As a result, our perfectly put together project starts splitting at the seams. That API server you had to save and fetch data suddenly needs to process reports from that data or do some correlation analysis. The service you had to store data into a Cassandra cluster has grown into a UI RPC server and is overloaded.
While this is just a maintainability issue at first, it can often degrade into a performance and stability problem. Constant refactoring places your project at risk for more bugs. A project that does a lot may have several developers in the same code repository, and this can lead to bad merges and slower releases, and it also makes the project hard to test, especially for regression cases. Scalability suffers because a customer information API service won’t scale the same way an external scanning and analytics server will.
So, is the end nigh? Will all of our projects be doomed to spiral into chaos? If you’re careful, you can stop this spiral early on, and when you do, you’ll find that your scale and product quality will reap the benefits (and so will your teammates!). You can reign in the monolithic madness if you know what behaviors to look for.
The first step is to identify when you’re doing something outside the pattern of your application. When your REST API server has a service layer that does some nice data merges or light data processing, it’s tempting to start putting more and more logic in there. Often, this happens gradually over time, but it’s easy to see that you’re doing something wrong when you’re adding new packages or your service files are growing insanely large. When this happens, take a step back and start to think how you would design this new feature if you were building a new service. Does it look completely different? Then, it’s more than likely you have yourself a new microservice.
Another pattern we’ve noticed is finding that you are stressing the scale or load of your system with a small subset of calls. Taking the API service as an example once again, you’ve been servicing simple REST calls for months with no indication of load issues, but now you’re noticing database contention or services struggling to keep up with the small analytics endpoint you added because it’s being called by another internal service 20K a second for three hours of the day. This might lead you to pursuing another microservice that can handle these requests individually.
- Look for patterns that don’t match your project architecture. If you are adding new packages or services to do something, it may not be the right project.
- Identify when you have multiple scaling patterns. If an application has two different scaling concerns, it will be very difficult to optimize your service to either one.
Obviously, you shouldn’t create a microservice for every function you want to write (and if you do, perhaps you should look into a serverless architecture) or every outlying pattern, but often these are the patterns that grow within our applications and turn them into nightmares.
Additionally, microservice architectures are not without their own woes. Sharing data between applications quickly and efficiently can cause a lot of complexity and fragility in your system or require a subsystem of your own. However, it’s important to remember why your original service was built and to try to identify when features are growing out of that scope.
The Road Beyond the Madness
All this being said, if you do find that dissecting your services into microservices works for you, having a solid foundation to build on is incredibly important. To that end, we recommend that you:
- Agree on libraries that you’ll be using in your services. This will keep new microservices from reinventing the wheel every time when it comes to sending HTTP requests or talking to a database, as well as building your project and managing its dependencies.
- Build clients and model packages into your project. If these can be packaged separately and distributed to other services in your architecture, it will help keep your data models accessible and up to date. Building these things into your service will keep all of this centralized and easy to maintain.
- Make spinning up new services light weight.. If your infrastructure team is getting overwhelmed trying to allocate new EC2 instances for your developers, then they won’t be able to do the vital work you need them to do.
- Don’t let your developers get siloed! Collaboration on this type of development increases your chances of identifying this problem early on. When developers are left to their own devices, we can find ourselves in a world of trouble we caused ourselves.