On Silver Bullets & Microservices

Thomas Meeks
Insightful Software
7 min readJun 21, 2018

--

Of all of the things I’ve learned developing software, scaling teams, and solving business problems over the years, the most important is this: never fall for the silver bullet.

Whether that silver bullet is TDD, messaging, REST, microservices, growth hacking, relational databases, non-relational databases, each must be considered a tool rather than the only right way to do things.

Microservices are my favorite thing to pick on these days. It is an important concept — especially for companies entering a growth stage and scaling up their development teams. However it can also be a giant, nasty, trap for early stage development if they aren’t careful with how they implement microservices.

Why Microservices?

Lots of developers doing work in one code base is chaotic. Getting 6 people to agree on a standard is difficult, getting 100 to agree is impossible. Plus, when you force a standard language or syntax on your entire company, your ability to hire and bring in new ideas is negatively affected. When your business can only work with the kind of developer that enjoys that narrow way of getting stuff done, innovation suffers. Microservices, like Service-Oriented Architectures before it, is a great way to scale out a developer organization.

On the operations side of your business, microservices allow independent scaling, higher degrees of resiliency, and faster debugging of production issues.

Microservices can be a solid win for your architecture too. It allows for clear, strong contracts between teams which greatly lessens the need for any one developer to understand the entirety of a company’s code. It borrows from the very successful UNIX philosophy (do one thing, do it well), enables wide reuse of libraries in any language, and speedier development in large organizations precisely because each individual service is simple, predictable, and easy to understand.

Why Not Microservices?

Network communication isn’t exactly efficient.

Just like the definition of Agile was hijacked ages ago, the community’s definition of microservices often implies separate applications communicating over a network.

Networks have been around for more than 40 years, and a lot of thought has gone into our false assumptions about communication over them that’s summed up well by the Fallacies of Distributed Computing:

Fallacies of Distributed Computing

The network is reliable.

Latency is zero.

Bandwidth is infinite.

The network is secure.

Topology doesn’t change.

There is one administrator.

Transport cost is zero.

The network is homogeneous.

Any one of the fallacies can present a major roadblock to a distributed application. As a result of these fallacies, when we develop distributed systems, we must grapple with an entirely new set of problems. If latency isn’t zero, how long should we wait? If the network isn’t reliable, how do we ensure data isn’t lost?

The solutions to these problems are orders of magnitude more expensive in terms of development, maintenance, and application speed relative to a method call.

Reliability is my favorite one to hammer precisely because it is complex to discern and takes hard work to get right. A naive microservice architecture might spin up one server for each service, for example, and quickly discover that the reliability of the overall application decreases simply because more things can go wrong.

Eventually that naive implementation turns into 2–3 servers with (hopefully) a couple load balancers, or 2–3 containers running within a Kubernetes cluster. Or perhaps you enjoy messaging and resolve to remove all synchronous calls from your application, only to discover that you’ve essentially created a giant distributed cache, and cache coherency is the hardest problem in computer science (right after naming things).

It is doable, sure, and it does make for a very solid system if you get it right. But that’s a lot of overhead compared to .do_the_damn_thing(with_this_parameter_i_do_not_have_to_serialize)

So this is a “Yay Monoliths!” Article?

No. Monoliths do have their place (remember: silver bullets), but it isn’t my preferred methodology.

For all their development speed, the faults of monoliths are well understood. As they grow in features, they inevitably grow in complexity. We come up with good reasons to cheat here and there, couple a few classes that shouldn’t be, and it all makes sense in the scope of one pull request. Eventually we take a step back, look at what we have created, and realize we have become what we hate.

My app seemed so friendly a year ago.

Although some of that slowdown is unavoidable as organizations grow, I’ve seen feature development on multiple such apps grind to a halt. Eventually this leads to a Big Rewrite, which is another way of saying, “We are going to shut down this product line in about a year”.

Internal Microservices are Pretty Great

Instead, create strong contracts internally to your system. In some cases, that might mean breaking out your code into libraries, but it doesn’t need to. It may be as simple as resisting the urge to let models directly know about one another, and handling those interactions at a higher level.

We can see how to apply this by looking at a slice of a training platform. Let’s look at updating course progress and emailing the user to celebrate a finished course. Controller and mailer code is omitted for brevity.

In a monolith it might look something like this:

It works; there’s a little cruft in there since the User model “knows” how to email, but this isn’t an uncommon pattern. Importantly, the mailer reads data directly from the User and Course models, which is quick to implement, but a block to ever distributing the code into different processes. Interestingly, I do feel like this solution is a little “prettier”, even though it is more difficult to maintain and scale.

In an internal microservice it might look something like this:

There are a few important things going on here. First, we don’t care about the implementation of User or Course. There are services, ideally with documentation, for us to interact with to get the information we need. UserService could, in fact, just as easily be a library or a shim to a remote API. Second, there is now a clear path to pulling emails out into their own service as well, given that it no longer reads directly from a model.

It is a touch less pretty, but there’s far less logic coupling, and with the recent advent of GDPR, it might be a good idea to keep a tight reign on personally identifiable information in a similar manner.

At a big enough scale, nobody can escape microservices (even 20 years ago, when they weren’t called that). When you get big, it will not be crushingly difficult to extract into a separate service — you’ll still have to pay the distributed service tax. But since you’re big, you can afford it, and finding the right time to pay a particular technical tax is a big part of building a successful technical organization.

Neat. Sounds like a good way to end up with a Monolith.

This is where the technical meets the human. It isn’t enough to just say “try really hard to not couple your code.” Good intentions get lost in the pressure to deliver and small trade-offs that seem expedient at the moment, but add up to big long-term problems.

Instead, what a team must implement is a rule along the lines of “packages must have a clear, public API that is the only method of communication between packages.” Implementation of such a rule is a blog post to itself, but it starts with ensuring the team understands why, has the time to voice their concerns and have them addressed, and agrees on a method of enforcement (probably code reviews, to start).

Seems like a lot of upfront work?

It is less than creating separate services, but more than creating a monolith. I’ve come to appreciate this pattern, however, because it avoids unreasonably sized models, intensely complex tests, and eases the ramp-up time for new developers on the project.

It also helps create good habits on the developer team — APIs tend to be clear and well-documented, tests tend to focus on a higher level and survive the occasional refactor.

When shouldn’t you do this?

When it doesn’t make sense! This isn’t a silver bullet either, and there are absolutely cases in which this level of separation is far more difficult to accomplish than the gain. Like anything else, it is a trade-off to be made.

A few good examples are:

  1. Speed and short-term goals matter more than everything else: for example, the company will not make payroll unless the application is done by some specific date.
  2. Spiking on a prototype to prove feasibility of a concept.
  3. It is extremely likely that the dev team for the application will never grow beyond ~4, and traffic will never grow beyond what a single database can handle. Both conditions should be true in this case, and it is a common reality for niche products.

Wrapping Up

One of the better ways to build a large application is to start with an internal microservice architecture. Doing so helps enforce good habits from day 1, makes scaling out to a distributed architecture later easier, and allows us to avoid the network tax at a critical moment in a project’s life.

--

--