In the last 5 years, “microservice architecture” has gone from cool buzzword to dangerous plague rife with misinformation, slipped deadlines, and unquestionably shittier end products.
A misplaced need for microservices drives organizations down the path of blindly scattering the business domain over the network. Production errors that would’ve been caught at compile time now arrear at runtime. Feature velocity is exchanged for defects. Every production incident turns into a murder mystery with unhelpful stack traces. Cash investments are made in distributed tracing, log and metric aggregation, and microservice orchestration. Agility, productivity, and eventually morale are slowly brought to their knees.
All while missing the mark on the one thing microservices really solve well: giving cross-functional teams total autonomy. Every other advantage is either exaggerated or a lie.
Effective modularity is a sliding scale. On one end, you expose internal data structures and confusing procedures. On the other, you have well-defined inputs and outputs, understandable interfaces, and a perfectly minimal surface area. Great modularity is plugging a brand new TV into the wall of your living room that’s a half-century old. Obvious, no confusion, and produces seemingly magical results. It balances local complexity with global, allowing us to develop a deep understanding of the problem space while knowing “just enough” about the outside world.
You know what has nothing to do with creating modularity? Networks. Ethernet cables. Packet switching. To think that microservices introduced any new ideas in modularity is absurd. Contending that they encourage modularity is technically true, simplistic, and lazy all at the same time. If senior technical leadership is feeding you this – find new leaders.
Introducing the network is a quick way to ensure teams respect module boundaries and consider the interfaces they expose. But don’t underestimate my ability to write distributed spaghetti code. The likelihood the end result has similarly confusing interfaces, more production incidents, and explodes overall system complexity1 is pretty high.
Languages Have Failed You
Too often we create concise, minimally-exposed modules in a codebase only to have someone reach into it months later and create a tight coupling somewhere naughty. Usually that person is us. It’s easy and it gets the job done, and that first broken window invites more people to do the same.
Repeat this over a few years with hundreds of developers and there’s no trace left of that understandable, well-defined interface with obvious inputs and outputs you once had.
Now might feel like a good time for microservices. Afterall, the network is the ultimate “don’t touch this fucking code” access modifier that not even Reflection could get through. Your “this is public for me, but not for you!” code is safe.
We put a lot of thought into all of the HTTP APIs we expose. They’re deliberate. Though the network can’t be the only answer. And, as Java developers probably know, it isn’t.
Martin Fowler’s been writing about the importance of public and published interfaces since 2002.
There’s something to be said for the public–published distinction being more important than the more common public–private distinction.
Java solved this issue with the
package-private visibility a long time ago, and C# kinda solved it with
internal. However almost every other popular language lacks a similar access modifier and most developers don’t even know they need it. Our languages suck at this.
It’s painful but not reason enough to jump to microservices. Shopify, a company employing thousands of developers, works on a 16-year old Ruby monolith and a few dozen microservices with boundaries so clear they call them “domain-specific apps”. They briefly considered breaking that monolith up into microservices in an attempt to address common complaints and maximize developer productivity. Equipped with years of experience in production microservices, they still opted for a different approach: making their monolith modular by solving the public vs. published interface problem. How pragmatic.
You thought you wanted microservices when, in reality, you just needed better access modifiers.
Modularity done right provides a lot of autonomy. We can imagine an org where teams of cross-functional specialists can be staffed in modules while not needing them to be experts in the whole system. Data scientists, engineers, and business analysts working together to figure out which movie I should watch next.
In this world, transitioning to a microservice has the potential to turn that module into an accelerant, unlocking greater agility and throughput. Teams are free to introduce new local complexity that might otherwise be impossible but highly valuable. Changing languages, runtimes, hardware, and development methodologies to better solve the problem. Releasing at different cadences thanks to new deployment units2. These freedoms come with price tags that might be worth your time and money if you think they’re what’s truly holding you back.
Or maybe your organization doesn’t need any of that.
I’m convinced companies misusing microservice architectures are conservatively spending 2x-5x what they should be on Engineering resources. Supporting six different languages when none solve the problem space particularly better than the other. Introducing service meshes for distributed tracing because stack traces have lost meaning. Building brand new microservices to deploy something faster when their monolith would equally benefit from modernized deployment procedures.
And finally, when it feels like it’s time to reassess the situation and go back to the drawing board, some companies will think to throw eventual consistency and asynchronous messaging into the mix to really put an end to those microservice problems once and for all3.
But seriously… microservices aren’t evil. You probably just don’t need them.