Microservices may be the new “premature optimization”
Microservices as a term and concept are still very much in a waxing phase of popularity. There are many companies trying to figure out how to bring the pattern to their project. But like many trends - the risk is that it becomes a solution in search of a problem. Let me say up front - I do believe that there is a lot of value to the pattern - I’m not going to rehash that here, this is covered plenty of places. Here I want to discuss something more about the when and the how a team chooses to invest in paying the microservices premium.
As a solution, microservices found its earliest and most authentic success in shops that independently converged on one or more aspects of the pattern organically, in response to problems they were facing. Not proactively by aggressively starting with microservices in early development. With the rise of microservices as a trend - lots of developers assume they can avoid pain, and increase productivity by starting with microservices day zero. But they may be inflicting on themselves more pain than they are avoiding.
I’m going to divide developers into two oversimplified groups. Startup developers and mature company teams (the big E). I’m going to focus on startups - but in the end, you’ll see why maybe that shouldn’t matter all that much. Startup companies, like their ideas, regularly sink or swim, but more often sink. McKinsey identified that companies need to grow fast or die. But growing fast, and learning to swim at the same time is not easy. The successful companies are the ones who can transition well between stages of growth - grind gears and die. Swimming the distance is hard.
In the early stages, paramount attention needs to go to picking and fitting the market, and rapid innovation to respond to early feedback. Anyone who has written software for a new project from scratch knows well just how much of it goes through major rewrite and refactoring. This is like dumping a puzzle consisting of fish parts (fins, tail, gills) in a pond, and figuring out how they need to go together to swim, before they sink (see also: plane assembly while flying). Lots of trial and error is a healthy necessity. Is this the right phase to be building with microservices? Probably not.
For one - early on, your team is likely small. Many of the valuable properties of microservices are as much about scaling the team as scaling your application. Martin Fowler recently wrote a great recap on the trade-offs of microservices. This is an excellent article, you should read it in full. However I think much of the current monolith vs microservices debate focuses too much on microservices adoption as a one-shot design choice, an either or. Instead I think it is more a question of when is the right time to make the commitment to adopt microservices, and how to set your team up for a successful transition.
If monoliths are often better for smaller teams, shifting domain boundaries, and early-stage complexity, and microservices better for large distributed teams working on complex and high scale problems, what can and should be done in the early stages of a project to set up for eventual microservices adoption? The TL;DR version of the answer is: adopt good SW practices - but let me spell some out as they specifically relate to the transition from a monolith to microservice pattern.
First and foremost - embrace proper modular design. This doesn’t require microservices, only informed organization of data, functions, and state. Grouping code into reused functions and then into libraries will set up a unit of work that can be eventually spun into a microservice. A primary focus: ensure that you understand what makes a good API. A crappy API in the form of poorly designed methods on a class, is going to suck even more when exposed as an HTTP API. Take advantage of the easier refactoring environment a monolith provides to get your domain boundaries understood and defined.
Sometimes it is assumed that writing applications as monoliths automatically leads to poorly organized code, but this is not the case. It might require more discipline to keep code well organized in a monolith - but spending cycles on paying the microservices premium on top of poor architecture tendencies likely will result in decreased overall productivity, and could result in a distributed ball-of-mud. Obviously there are degrees of “Monolith”, but I think the monolith first idea doesn’t assume you will write your entire app as one giant function, there can be ranges of architectural quality under the term “monolith”.
Establish early notions of local-expertise and ownership in the codebase. Even if it is just one developer who is the “login page” expert - it sets the tone for separate teams owning different concerns in the app overall. Enforce that modules access the state owned by other modules only via API, not by accessing data directly. This will cultivate the expectation of data access across domains only through contract and API.
“Devops” is not an all or nothing prospect One way to think of microservices
is “SOA + Devops”. Devops is another term and pattern that has different values and manifestations for different sized teams and project complexity. Remember that when starting a new project, that you are not Google or Netflix. Start with investing in what will most set you up for well oiled devops down the road, namely testing and automation, in any form you can. Many devops practices build on each other in a rough dependency chain:
Testing hygiene > Continuous Integration > Continuous delivery > Continuous deployment
Don’t sacrifice good execution testing practices in the push to get working continuous deployments. Your team has a lot to get done early on. Building solid foundations towards your ultimate devops state is “technical investment” - the opposite of “technical debt”.
While a monolith can’t employ functionality like canary releases of a single service to test new functionality - your team can still make use of a feature-flipper functionality, which many find useful even in a microservices environment.
Having excellent visibility into how your app runs does not require microservices, but becomes even more critical once in a microservices environment. You can early on establish centralized logging and monitoring infrastructure outside the direct runtime context of your application. If you look at Adrian Cockcroft’s key features of microservices monitoring - most could apply to a monolith equally well, they are just more critical in high complexity situations where microservices are likely to be in the mix.
You can even set up an early version of distributed tracing, in the form of traces between modules. Even just recording stacktraces with something like Sentry is a start in the right direction. Having this understanding of how information flows through your system is useful when debugging performance issues, you won’t be able to drop into a debugger or simply do a dump or stacktrace once you have a distributed system of microservices.
Microservices will involve a certain degree of isolation of teams from each other by design, this is due to the primary limited association of teams with service(s) they own, but may also include geographic and TZ separation of teams, with async communication. This can increase the friction on informal communications and productive forms of tribal knowledge. Conversely the software itself is intended to be highly communicative, making calls between different services. The make-up of teams will always be changing. The expectation is an environment of high growth - with regular onboarding of new developers, and the standard high churn of the current tech world - members may also move somewhat regularly between service teams.
All of this points to a requirement for solid documentation as a standard for well functioning teams. Part of this might come in the form of an API framework that publishes API documentation to a common clearinghouse. But requirements gathering, design docs, usage guides, performance objectives, etc should all be documented both for new service-team members, as well as for developers working on other services. Again - nothing about these habits and systems need microservices to start being useful.
You should never take shortcuts or defer security as a concern in your software design. Some security practices seem to arise only as a team grows, and you realize only then that maybe you should make some changes. It is easy to assume incorrectly, that these items might be coupled to team organization as much as microservices. You should avoid giving out keys to the kingdom - principles of least privilege should start with yourself acting in different contexts (am I routinely pushing minor version, or forensically examining a down host), and apply to the software you launch. This is all a good idea even in a team of one, but becomes magnified in importance as the complexity of the software and teams increases.
Understand and design your deployment process so that the act of deploying itself is treated as an managed explicit privilege. Use features like service accounts and roles to give machines access to systems instead of ever committing and deploying secrets. Use secrets management tools to guard and rotate secrets like encryption keys and API keys. Establish a common code path for API authentication and authorization, this can later be incorporated into a microservices gateway. Since you already set up centralized logging above, you should of course to be careful that you never leak sensitive information or personally identifiable information (PII) in logs.
This isn’t meant to be an exhaustive FAQ - but here are a couple of additional points in the form of Q&A format.
Q: Isn’t it better to avoid the rework of code into microservices, and instead just start with the pattern?
A: Software always involves rework. When we read posts by maturing startups about “upgrading” their development towards various “big kid” systems, we tend to focus on the improved outcomes as being the best place to start. For example, look at this post from sendwithus. It is easy to conclude that maybe they would have been better off avoiding a bunch of rework by just starting with dynamodb. But look at the benefits postgres gave them at the beginning, and look at how well they understood exactly what they needed by the time they switched. Also - refactoring code is much easier to do within a single team/codebase/monolith than it is to reorganize service boundaries/APIs/teams - which is essentially what refactoring entails in microservices.
Q: What about handling scale - won’t the scale out nature of individual services allow the overall system to scale much better?
A: Yes - if the system boundaries are well chosen, and services well monitored, this can be one of the advantages. But scale, performance, and load capacity are all pretty well entangled concepts. The distributed nature of microservices comes with a latency performance penalty. Making the assumption you can compensate for this latency by scaling out capacity can be a very expensive misconception. An excellent counter-example that shows how scaling up, not out, with a focus on performance as a response to demand load is Stack Overflow’s approach to scale.
Concluding thoughts and large org teams
Most of the above are just good things to do, and are largely orthogonal to microservices adoption. Sometimes people will look to use microservices as a way to goad themselves to do the right thing - like signing up for gym membership so that the guilt of the monthly bill will force you to workout. But doing this risks a misguided motivation that hampers effective adoption adoption, or worse, can lead to outright absence of care if people assume that “we use microservices, so this takes care of X for us automatically for free”. There is also a tendency for teams to embrace complexity prematurely (instead of defending simplicity), and to assume that using microservices and the assorted trappings is a sign of sophistication in a form of microservice envy. With microservices first, many of the above suggestions that contribute to the microservices premium need to be sorted out up-front. But if you start with a monolith - some of the above can be put in place in phases and iterations, allowing the team more cycles for the more precious product development early on.
How much of all this applies to teams in large, mature engineering organizations? This can vary and depends on a number of factors extant in the current environment. Is the domain very well understood? Is this the third rewrite of this system in 8 years - this time as microservices? Are there other teams and toolchains at the company already using microservices - being able to make use of mentors and existing infrastructure can reduce the burden of microservices uptake. Is this a new project with clean slate? Even in a large organization, operating like a startup has its advantages. Acting as a startup might mean starting with a monolith during the early days of a developing the product domain with low complexity and heavy refactoring. Do you have a larger engineering culture that contains some strong devops anti-patterns, such as rigid, infrequent release cycles - inability for developers to deploy, sluggish change management, etc? Break those down first, use microservices as a wedge if you must - but you can’t successfully use microservices if those points of friction are in place. Be very careful when “porting” to microservices. Strong couplings and nasty entanglement can easily follow a project if it aims to base a microservices port on a problematic (poorly modularized), overly complex monolith. The choice to start by just peeling off some things around the edges into microservices can work, but make sure you are not setting yourself up for copying poor domain boundaries by leaving an intractable core once all the low hanging outer layers are split off.
"We should forget about small efficiencies, say about 97% of the time: premature optimization is the root of all evil. Yet we should not pass up our opportunities in that critical 3%" -Knuth, Donald (December 1974)
Similar to the pitfalls and reality of software optimization, teams should approach microservices with a good deal of caution, not shiny abandon. Don’t distract yourself at early stages from putting forth effort where it has the most value in building your application, but be thoughtful and plan for complexity by actively and intentionally developing good habits.