Microservices: Hidden Technical Debt (and how to possibly avoid the traps)

Introduction

These days, I keep seeing microservices being treated as the answer to every software problem. Having worked with various architectures throughout my career, I've noticed how many players often jump into microservices without considering the long-term implications. It reminds me of a project where we turned a perfectly functional monolith into a distributed system just because "that's what modern companies do."

But here's the thing: microservices aren't a silver bullet. In fact, they can become a massive technical debt that's incredibly hard to pay off. Matt Ranney, DoorDash's Scalability Engineer Lead, makes this point brilliantly in his talk "Microservices are Technical Debt." After experiencing similar challenges, I decided to dive into this topic, including some cientific papers that covered similar issues like "Microservices Anti-Patterns: A Taxonomy," to understand what's really going on.

In this post, I'll try to cover:

  1. Why microservices are often misunderstood and misused
  2. Common anti-patterns I've encountered (and how to avoid them)
  3. Evidence-based approaches to build systems that actually work

The Overuse of Microservices

Why Do We Default to Microservices?

  1. The Hype Factor

    • Companies often adopt microservices because it's trendy, without analyzing their actual needs.
    • Example: Imagine you’re at a startup trying to attract investors. Your team decides to split a simple app into 10 microservices just to showcase "scalability." Fast forward six months: you’re drowning in Kubernetes configurations and service mesh setups, while competitors with monoliths ship features twice as fast.
  2. The Independence Illusion

    • While microservices promise independent teams and deployments, this only works with proper service boundaries.
    • Example: Picture working on a user service. You update a field in the UserProfile class, only to discover the notifications service crashes because it hardcoded a dependency on the old field structure. Now you’re stuck updating three services for what should have been a simple change.
  3. The Monolith Misconception

    • I've noticed a strange fear of monoliths in the industry. But here's what I've learned: for many applications, a well-structured modular monolith is actually the better choice.
    • Example: A software development team spent months breaking their monolith into microservices, only to realize they had created a distributed monolith—a system that was just as hard to manage, but with added complexity. What if it was just a simplified collection of function calls instead of a bunch of meaningless HTTP calls?

Microservices as Technical Debt

Matt Ranney makes a compelling case for why microservices can be considered technical debt. Here’s why:

  1. Initial Speed, Long-Term Pain
    • Microservices can speed up development in the short term, but they often lead to long-term maintenance challenges.
    • Example: Imagine you’re building a feature to let users reset passwords. You start with a simple service:
    public void resetPassword(String userId, String newPassword) {
         User user = userService.getUser(userId);
         try{
           userService.updateProfile(user, newPassword);
         }
     }

But obviously the requirements grow and now this evolves into:

  // look at the complexity added to the same function
  try {
      userService.updateProfile(user, newPassword);
      notificationService.notifyProfileUpdate(user.getId());
      authService.refreshUserSession(user.getId());
      analyticsService.trackProfileUpdate(user.getId());
  } catch (ServiceException e) {
      // now you need complex rollback logic =D
      compensationService.handleFailure(user.getId(), "PROFILE_UPDATE");
  }

Now, a simple password reset requires four services to work perfectly together. Miss one, and you’ve got angry users or security holes and a fresh war room to deal.

2. The Distributed Monolith Trap

Let’s get real: most companies end up with distributed monoliths, not true microservices. Here’s why this happens and why it’s worse than a traditional monolith.

Practical Example: The Loyalty Points Nightmare

Imagine you’re working on an e-commerce system. You need to add a loyaltyPoints field to user profiles. Here’s what happens:

  1. User Service:

    public class User {
        private String id;
        private int loyaltyPoints; // New field
    }
    
  2. Payments Service:

    public class PaymentProcessor {
        public void applyDiscount(String userId) {
            User user = userService.getUser(userId);
            if (user.getLoyaltyPoints() > 1000) { // Now depends on User's new field
                applyDiscount();
            }
        }
    }
    
  3. Analytics Service:

    public class Analytics {
        public void trackPurchase(String userId) {
            User user = userService.getUser(userId);
            log("Purchase by user with " + user.getLoyaltyPoints() + " points");
        }
    }
    

Suddenly, updating a single field requires:

  • Coordinating deployments across three teams
  • Ensuring all services update dependencies simultaneously
  • Risking system-wide failures if any service lags

This is the distributed monolith trap—a system with all the complexity of microservices but none of the benefits. As Newman (2021) notes, this anti-pattern is rampant in teams that prioritize speed over thoughtful design.

3. Hidden Costs of Microservices

  • Microservices introduce hidden costs, such as network latency, service discovery, and inter-service communication.

  • Example: Imagine you’re debugging why user sessions expire randomly. After days of checking code, you discover a 200ms delay between the auth service and session service. The timeout configuration didn’t account for this latency, causing sporadic failures. The fix? Hours and hours wasted of meaningless debugging time, as the root cause for the problem was a bad-optimized code deployed by the auth team.


Common Anti-Patterns in Microservices

The paper Microservices Anti-Patterns: A Taxonomy by Taibi et al. (2018) provides a solid framework for understanding these issues. Here are some key anti-patterns and real-world examples:

1. The Shared Database Anti-Pattern

  • One of the most common mistakes is sharing databases between microservices. This creates tight coupling and defeats the purpose of having independent services.
  • Example: Imagine you’re working on a notifications service that shares a database with the user service:
   -- Both services read from the same table
   SELECT email FROM users WHERE id = '123';
  • When the user service adds a new is_email_verified column and starts deleting unverified accounts, your notifications service starts failing because it wasn’t updated to handle the new logic.

2. Hardcoded URLs and Tight Coupling

  • Hardcoding URLs or endpoints between services is a recipe for disaster. It creates tight coupling and makes the system more fragile.
  • Example: Picture this code in your payments service:
   // Bad: Hardcoded URL
   String userServiceUrl = "http://user-service-prod:8080/api/users";
  • When you try to test this service locally, it fails because it can’t reach the production user service.

3. The "Too Many Services" Problem

  • This one is a classig example... Splitting your system into too many tiny services can lead to chaos. Each service adds overhead in terms of deployment, monitoring, and maintenance.
  • Example: Imagine you’re working on a food delivery app with these services:
    1. user-service
    2. restaurant-service
    3. menu-service (for restaurant menus)
    4. menu-item-service (for individual dishes)
    5. menu-category-service (for dish categories)

Now, displaying a restaurant’s menu requires calls to three services. A simple feature like adding a new dish category takes weeks to implement across teams.

4. Lack of Governance

  • Without proper guidelines, teams end up creating services that overlap or don’t integrate well.
  • Example: A very known company had two teams building nearly identical services because there was no governance in place to coordinate their efforts.

Solutions and Best Practices

So, how do we avoid these pitfalls? First, it's important to recognize that both monolith decomposition and microservices modeling are complex fields, extensively studied in research and industry, but in general here are some widely adopted strategies:

1. Monolith First

  • As Martin Fowler suggests, "monolith first." Build a monolith, and only split it into microservices when necessary.
  • Example: Imagine you’re building a new fitness tracking app. Start with a monolith:
   public class Workout {
      private String userId;
      private LocalDateTime startTime;
      private int durationMinutes;
   }
  • Only split into microservices when:

    1. Different components have clearly different scaling needs.

    2. Teams are large enough to justify separate ownership.

    3. There's a structured governance process in your company over building decoupled services.

    4. Automated tests are available for each service.

    5. Automated deployments are available for each service.

    6. Live monitoring, distributed tracing and health checks are available for each service.

    7. Automated rollback during deployment are also available for each service.

2. Domain-Driven Design (DDD)

  • Define clear boundaries for each service based on business domains. This helps avoid tight coupling and ensures that services are truly independent.
  • Example: For an e-commerce platform:
    1. Bounded Context: Payments
    2. Bounded Context: Inventory
    3. Bounded Context: Shipping

Each context has its own database and API boundaries. Changes to payment logic don’t affect shipping =D.

3. The Reverse Conway Maneuver

  • The Conway’s Law states that organizations design systems that mirror their communication structures. The Reverse Conway Maneuver flips this around: design your teams to match the architecture you want, basically, the teams designing a go-to system architecture based on current needs, tech debts and throttles. Just like a "reference architecture" based on reverse engineering. This way you have clear boundaries designed for each team and let they execute their goal software architecture independently.
  • Example: Consider a given company X, instead of having frontend, backend, and ops teams working in silos, they restructured teams around business capabilities: a 'Payments Team' owning both backend and UI for payments, and a 'Shipping Team' handling logistics end-to-end. This allowed them to scale services independently while keeping architecture cohesive.

Conclusion

I didn't mean to be the devil’s advocate here, but I tried to highlight some key points because, in many cases, the direction that microservices adoption takes ends up being unsustainable. That’s why it’s crucial to pay attention to the trade-offs and pitfalls discussed.

This is not supposed to be an exhaustive analysis—far from it. As microservices, technical debt, and distributed architectures are vast and evolving fields, hundreds of thousands of cientific papers already discussed these topics. My goal was to cover some of the major issues I’ve encountered, and hopefully, this discussion helps you navigate the complexities of microservices with a more critical perspective.


References

  1. Matt Ranney, Microservices are Technical Debt link.
  2. Taibi et al., Microservices Anti-Patterns: A Taxonomy (2018).
  3. Martin Fowler, Monolith First link.
  4. Conway’s Law and the Reverse Conway Maneuver (Various Sources).