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, given enough time and changing features, 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
Projects start with great intentions. We create concise, minimally-exposed modules in our codebases. They’re well considered and intentional. A few months later, someone reaches in and creates a tight coupling somewhere naughty. Usually that person is us. It’s easy and it gets the job done under a looming deadline.
That first broken window invites more people to do the same.
Repeat this over a few years with tens or hundreds of developers and there’s no trace left of that understandable, well-defined interface with obvious inputs and outputs you once had. Everything is public. And if it isn’t yet, it’s just a matter of time. You and your colleagues start having nightmares about the legacy monolith while watching every product deadline slip past you.
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. Forever.
Knowing it’ll be really painful to get a networked API wrong, we put a lot of thought into what’s exposed. Everything is deliberate. We talk to our API consumers, propose drafts, incorporate feedback, then version everything.
Publishing an interface is a one-way door. Unpublishing an interface is not an option.
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. You should’ve just learned Java2.
The lack of published-public is 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. If your language doesn’t have them, go shoehorn them into your project just like Shopify did.
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 units3. 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.
Companies misusing microservice architectures are conservatively spending 2x-5x what they should be on Engineering resources. A team of 30 doing what 8 could do4. 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. Debugging complex orchestrators overseeing the lifecycle of a dozen containerized services and you have no idea why it’s not working as you’d expect.
And finally, a brick wall hits. Your feature development has similarly slowed down to a crawl. Now feels like a good time to reassess the situation and go back to the drawing board about microservices. Or you just double down, hire more developers, throw eventual consistency and asynchronous messaging into the mix to really put an end to those microservice problems once and for all5.
But seriously… microservices aren’t evil. You probably just don’t need them.
I’m joking… ↩︎
Have you seen what Basecamp is capable of? Jeez… You’d think there’s 100 developers working for them. ↩︎
By “problem” I mean your business. ↩︎