Lessons from a Decade of Complexity: Microservices to Simplicity

10 min read Original article ↗

Microservice architecture has been a topic of many discussions and different opinions among software engineers. A decade ago, microservices were seen as a way to solve all the challenges of building and scaling large systems. Many teams believed it was the future. But now, after seeing how it plays out in real-world situations, many of us, including my team, are starting to question whether it’s still the best choice.

In this post, I want to walk you through what I’ve learned from working with microservices in different organizations. I’ll talk about why it became popular, the problems it was meant to solve, the challenges we faced, and how we’re now rethinking our approach.

Let’s go back to about a decade ago. At that time, microservices were everywhere. Consultants praised it, companies showcased success stories, and everyone seemed convinced. Tech talks, blog posts, and consultants were all promoting this architecture as the next big thing. The benefits advertised sounded amazing:

  • Smaller teams could work independently.
  • Faster deployments because each service was smaller.
  • Freedom to choose different programming languages and tools.
  • Better reliability since services could be isolated and scaled separately.
  • Continuous delivery without downtime.

And it’s true, many of these benefits were real. A lot of companies saw good results early on, especially big tech companies like Google, Netflix, Meta, Uber, and Amazon. Their success stories made the architecture look even more attractive, and naturally, many other companies wanted to follow their path.

The benefits were exciting, but when I look back, I believe many companies didn’t adopt microservices just because of technical reasons. It was more about solving people and team problems.

Back then, the tech industry was booming. Startups were rapidly growing, funding was everywhere, capital was cheap, and their priority was speed. Every tech company wants to launch products as quickly as possible. The easiest way to go faster was simply hiring more engineers.

But more engineers working in the same codebase created significant challenges:

  • Monolithic codebases became slow to build and deploy, slowing down entire teams.
  • More engineers meant more conflicts and communication overhead; people started to step on each other’s feet, slowing productivity even further.

Microservices offered a quick solution to these problems: splitting the big, complex codebase into smaller, manageable chunks.

So, instead of fixing the core architecture of the monolith, the quick fix was to break the system into smaller pieces, microservices. It worked. This helped teams work more independently and move faster. But while it solved some problems, it also introduced new ones. Managing many small, separate services turned out to be complex and came with its own set of challenges.

Did Microservices deliver in the long run?

At first, everything felt better. We could deploy services faster, experiment more, and scale parts of the system independently. But after a few years, the downsides started to pile up.

Too many “tiny” services

In many situations, we ended up with services that were too small. Some only handled one or two API endpoints. This created a lot of extra work for engineers. Each service had its own repo, its own deployment process, its own monitoring setup, and its own set of alerts. Even the databases were overly split, with some having just one table. All of this added overhead for the engineers, taking their time away from what really matters: solving real problems for users. In the past, we had a situation where one Backend engineer needed to handle 4 services on average. It wasn’t sustainable.

Reliability didn’t improve

The idea behind microservices is that if one service fails, it shouldn’t affect the rest of the system. But in reality, we often saw the opposite. One small failure could trigger alerts in many other services that depended on it. I remember situations where a single service had issues, and because it was used by multiple other services, alerts fired across the board. All the on-call engineers were woken up, but none of them could fix anything until that one broken service was healthy again.

In theory, each microservice should be isolated. If one goes down, the rest should continue working fine. But in practice, this rarely happened. At least in my experience, we didn’t end up with cleanly separated systems, we ended up with something that looked more like a distributed monolith. Instead of isolated services, we ended up with complex systems tightly coupled by network calls instead of direct function calls. That made everything more complex and fragile.

Network complexity

In monolithic applications, different parts of the system talk to each other using direct function calls, which are very fast. But in a microservices setup, these calls turn into network requests, which are much slower and can introduce more points of failure. As the number of services grows, so does the number of network calls.

In one example from our system, loading the homepage triggered over 300 network calls. Even though every service optimized the latency, the total added up to around 600ms, far above our target of 100ms for critical APIs. To fix this, we had to add caching layers and aggregators to reduce the fan-out. But when we looked at the problem again, we asked ourselves: is this even a problem we should have in the first place?

We were spending time and effort fixing issues that were created by the architecture itself. This reminded me of a quote from Elon Musk: “The most popular mistake of great engineers is solving problems that shouldn’t exist.”?

Operational and maintenance overhead

As the number of services increased, so did the complexity of managing them. Each service required its own monitoring tools, logging setup, deployment pipeline, and alerting system. This added a huge operational burden and significantly increased costs. Even tools like Kubernetes couldn’t fully solve the problem.

At first, orchestration made things seem more manageable. But many of our services were so small that even assigning just 10 millicores of CPU felt wasteful. Most of them never used even 30% of what they were allocated. Yet, each still took up a slot on a worker node, making it hard to plan and utilize resources effectively.

We also ran into issues from taking domain-driven design to the extreme. Data models were split too strictly by domain. As a result, one service often needed to call several others just to gather a small piece of information. This increased the number of dependencies and made the call graph deeper and harder to manage. In some cases, the team responsible for a domain didn’t have time to build a new API, and that blocked others from moving forward. It showed us that architecture isn’t a magic fix, the way we execute and collaborate matters just as much.

On top of that, maintaining and upgrading our systems became harder. Updating a shared library or framework, like Node.js, meant doing the same task over and over across hundreds of services. It took weeks, even months. And if one service changed its API, multiple other teams had to coordinate updates to keep everything in sync. It made even simple changes painfully slow.

The Trigger

Microservices worked well during a time when companies had plenty of funding and were focused on growing fast. But things changed after the pandemic. With tighter budgets, companies can’t just add more engineers or spin up infrastructure whenever needed. The focus shifted to building smaller, more efficient teams. Many companies started reducing headcount, and suddenly, the overhead of managing hundreds of microservices no longer made sense.

This shift made us stop and think: if fast growth isn’t the priority anymore, is microservices still the right choice?

At the same time, AI has been evolving quickly. As I mentioned in another post, the more context you can give to an AI model, the better the results it produces. This made companies start to see the value in larger, unified codebases. With fragmented microservices, that context is spread too thin. But with a consolidated codebase, you can give AI tools better visibility into the system, which can actually speed up development. So now, consolidation is becoming a priority, not just for operational simplicity but also to make better use of AI in software development.

The Direction

As things changed, we realized it was time to rethink our architecture. Instead of sticking with hundreds of small services, we started consolidating them. The goal was simple: reduce the number of codebases and deployments and make better use of our resources.

We began merging workloads from many microservices into fewer, more manageable services. We also consolidated databases where it made sense. This made it easier to fine-tune performance, manage resources, and reduce overhead. With fewer services, we had fewer repositories, fewer alerts to handle, and simpler dashboards to monitor. This made life much easier for the team.

One approach that worked well for us was to group services into blocks. Each block contains a few domain-focused subapps that live inside a single, larger codebase. Engineers work together within that shared codebase. If one subapp grows too large, we can split it out later as its own service. But we only do that when it’s really needed. Until then, it stays inside the block.

This approach gives us the best of both worlds: the simplicity of a monolith with the flexibility of modular design. It keeps things organized, easy to manage and allows teams to move quickly without the heavy overhead that comes with microservices. The benefit that we got so far,

  • We went from hundreds of services down to fewer than ten.
  • That helped us cut down alerts, simplify deployments, and make better use of infrastructure.

Lesson Learned

After going through years of building and maintaining systems with microservices, we’ve learned a lot, especially about what really matters in choosing an architecture. Here are some key takeaways that guide how we think about system design today:

Be pragmatic, not idealistic: Don’t get caught up in trendy architecture patterns just because they sound impressive. Focus on what makes sense for your team and your situation. Not every new system needs to start with microservices, especially if the problems they solve aren’t even there yet.

Start simple: The simplest solution is often the best one. It’s easier to build, easier to understand, and easier to change. Keeping things simple takes discipline, but it saves time and pain in the long run.

Split only when it really makes sense: Don’t break things apart just because “that’s what we do”. Split services when there’s a clear technical reason, like performance, resource needs, or special hardware.

Microservices are just a tool: They’re not good or bad by themselves. What matters is whether they help your team move faster, stay flexible, and solve real problems.

Every choice comes with tradeoffs: No architecture is perfect. Every decision has upsides and downsides. What’s important is to be aware of those tradeoffs and make the best call for your team.

At the end of the day, our job as engineers is to build things that work and bring value. That means staying flexible, thinking clearly, and always choosing what’s most effective, not what’s most trendy. Always putting simplicity first.


Discover more from

Subscribe to get the latest posts sent to your email.