The Microservices Backlash: Over-Engineering or Misunderstood Architecture?

Introduction Microservices architecture has been one of the most debated topics in software engineering over the past decade. While many organizations have successfully adopted this pattern, there's growing skepticism about whether microservices are often an over-engineered solution to problems that could be solved with simpler architectures. This article explores why microservices generate so much criticism, examines valid concerns about over-engineering, and presents alternative approaches like clean three-layer architecture that might offer better solutions for many projects - with a powerful ally that simplifies it. TL;DR Skip to the end to meet the ally... but if you do, you'll miss: Why your Kubernetes cluster gives you nightmares How foreign keys became the villains of our story The secret monolith-master race manifesto Why 90% of microservices are just distributed monoliths in disguise Why People Hate Microservices 1. Complexity Overhead Microservices introduce significant operational complexity that many teams aren't prepared to handle. Suddenly, you need to manage: Service discovery Distributed tracing Circuit breakers API gateways Cross-service transactions Eventual consistency patterns For small to medium-sized applications, this complexity often outweighs the benefits. "We spent 3 months setting up observability before writing business logic" – Anonymous Engineer 2. Infrastructure Costs The criticism about Kubernetes costs is valid for many cases. Running multiple services in production requires: Container orchestration More sophisticated monitoring Additional network infrastructure Potentially more expensive hosting solutions While you can run microservices on a single EC2 instance, this often defeats the purpose of having independent scalability. 3. Development Experience Degradation Developers frequently complain about: Needing to run multiple services locally Difficulty in debugging distributed systems More complicated CI/CD pipelines Context switching between multiple codebases 4. Premature Scaling Many teams adopt microservices because "we might need to scale," not because they actually need to scale. This premature optimization leads to unnecessary complexity. The Over-Engineering Problem Microservices have become a victim of their own hype cycle. Many teams implement them because: Resume-driven development ("FAANG uses them!") Architecture theater (Impressive-looking diagrams) Premature optimization ("We might need to scale someday") Reality Check: Less than 5% of applications truly benefit from microservices initially. Rarely do teams honestly assess whether their problem actually requires a distributed systems approach. The truth is that most applications never reach the scale where monoliths become problematic, and when they do, careful modularization within a monolith can often solve the scaling issues. Modular Monoliths: The Balanced Approach Your observation about structuring code for potential future separation is insightful. This approach—often called the "modular monolith" pattern—provides many benefits: Single codebase: One repository with clear internal boundaries Simplified deployment: One binary/Docker image to manage Easier testing: No need for complex integration test setups Future flexibility: Well-structured code can be split later if truly needed In Go, this could look like: /project ├── main.go # Single entry point ├── order/ # Fully self-contained │ │ ├── handler/ │ │ ├── service/ │ │ └── store/ │ ├── main.go # Optional microservice entry point │ └── Dockerfile # Optional microservice Dockerfile │ ├── catalogue/ # Independent domain │ └── customer/ # Clear boundaries └── Dockerfile # Single deployment Clean Three-Layer Architecture in Practice 1. Delivery Layer (Handler) HTTP/gRPC endpoints Validation Example: type service interface { CreateOrder(ctx *gofr.Context, order Order) (error) } func (h *OrderHandler) Create(c *gofr.Context) (any,error) { var req OrderRequest if err := c.Bind(&req); err != nil { return nil,http.ErrorInvalidParam{Params: []string{"body"}} } // Delegates to service layer return nil, h.service.CreateOrder(ctx, req) } 2. Business Logic Layer (Service) Manages all domain relationships Database-agnostic (critical for flexibility) Anti-pattern alert: -- ❌ Avoid foreign key constraints CREATE TABLE orders ( customer_id INT REFERENCES customers(id) # Tight DB coupling ); Why? Database-level constraints like foreign keys create tight coupling that violates service boundaries - always enforce relationships in your application code, not your storage layer. Better: // ✅ Service manages validation type customerService inte

Apr 30, 2025 - 00:17
 0
The Microservices Backlash: Over-Engineering or Misunderstood Architecture?

Introduction

Microservices architecture has been one of the most debated topics in software engineering over the past decade. While many organizations have successfully adopted this pattern, there's growing skepticism about whether microservices are often an over-engineered solution to problems that could be solved with simpler architectures. This article explores why microservices generate so much criticism, examines valid concerns about over-engineering, and presents alternative approaches like clean three-layer architecture that might offer better solutions for many projects - with a powerful ally that simplifies it.

TL;DR Skip to the end to meet the ally... but if you do, you'll miss:

  • Why your Kubernetes cluster gives you nightmares
  • How foreign keys became the villains of our story
  • The secret monolith-master race manifesto
  • Why 90% of microservices are just distributed monoliths in disguise

Why People Hate Microservices

1. Complexity Overhead

Microservices introduce significant operational complexity that many teams aren't prepared to handle. Suddenly, you need to manage:

  • Service discovery
  • Distributed tracing
  • Circuit breakers
  • API gateways
  • Cross-service transactions
  • Eventual consistency patterns

For small to medium-sized applications, this complexity often outweighs the benefits.

"We spent 3 months setting up observability before writing business logic" – Anonymous Engineer

2. Infrastructure Costs

The criticism about Kubernetes costs is valid for many cases. Running multiple services in production requires:

  • Container orchestration
  • More sophisticated monitoring
  • Additional network infrastructure
  • Potentially more expensive hosting solutions

While you can run microservices on a single EC2 instance, this often defeats the purpose of having independent scalability.

3. Development Experience Degradation

Developers frequently complain about:

  • Needing to run multiple services locally
  • Difficulty in debugging distributed systems
  • More complicated CI/CD pipelines
  • Context switching between multiple codebases

4. Premature Scaling

Many teams adopt microservices because "we might need to scale," not because they actually need to scale. This premature optimization leads to unnecessary complexity.

The Over-Engineering Problem

Microservices have become a victim of their own hype cycle. Many teams implement them because:

  • Resume-driven development ("FAANG uses them!")
  • Architecture theater (Impressive-looking diagrams)
  • Premature optimization ("We might need to scale someday")

Reality Check:

Less than 5% of applications truly benefit from microservices initially.
Rarely do teams honestly assess whether their problem actually requires a distributed systems approach. The truth is that most applications never reach the scale where monoliths become problematic, and when they do, careful modularization within a monolith can often solve the scaling issues.

Modular Monoliths: The Balanced Approach

Your observation about structuring code for potential future separation is insightful. This approach—often called the "modular monolith" pattern—provides many benefits:

  1. Single codebase: One repository with clear internal boundaries
  2. Simplified deployment: One binary/Docker image to manage
  3. Easier testing: No need for complex integration test setups
  4. Future flexibility: Well-structured code can be split later if truly needed

In Go, this could look like:

/project
├── main.go          # Single entry point
├── order/           # Fully self-contained
│   │   ├── handler/
│   │   ├── service/
│   │   └── store/
│   ├── main.go      # Optional microservice entry point
│   └── Dockerfile   # Optional microservice Dockerfile
│   ├── catalogue/   # Independent domain
│   └── customer/    # Clear boundaries
└── Dockerfile       # Single deployment

Clean Three-Layer Architecture in Practice

1. Delivery Layer (Handler)

  • HTTP/gRPC endpoints
  • Validation
  • Example:
  type service interface {
      CreateOrder(ctx *gofr.Context, order Order) (error)
  }

  func (h *OrderHandler) Create(c *gofr.Context) (any,error) {
      var req OrderRequest

      if err := c.Bind(&req); err != nil {
          return nil,http.ErrorInvalidParam{Params: []string{"body"}}
      }

      // Delegates to service layer
      return nil, h.service.CreateOrder(ctx, req)
  }

2. Business Logic Layer (Service)

  • Manages all domain relationships
  • Database-agnostic (critical for flexibility)
  • Anti-pattern alert:
  -- ❌ Avoid foreign key constraints
  CREATE TABLE orders (
      customer_id INT REFERENCES customers(id)  # Tight DB coupling
  );

Why? Database-level constraints like foreign keys create tight coupling that violates service boundaries - always enforce relationships in your application code, not your storage layer.

Better:

  // ✅ Service manages validation
  type customerService interface {
      GetByID(ctx *gofr.Context, id string) (Customer, error)
  }

  func (s *OrderService) Create(ctx *gofr.Context,input OrderInput) error {
      exists, err := s.customerService.GetByID(ctx,input.CustomerID)
      if err != nil {
          return err
      }
      // Proceed with order creation
  }

Practical Microservice Division: Your E-commerce Example

  1. Order Service: Focused purely on order processing
  2. Catalogue Service: Manages products and brands (cohesive domain)
  3. Customer Service: Handles all customer-related data

This division follows domain boundaries rather than technical concerns. The key insight is keeping tightly coupled entities together (like brands and products) while separating domains that can evolve independently (like orders and customers).

When Microservices Deployment Make Sense

Microservices Deployment become valuable when:

  • Different components have genuinely different scaling requirements
  • Teams need to work independently with different release cycles
  • You have clear domain boundaries with minimal cross-domain communication
  • You're at a scale where operational overhead is justified

Conclusion

The backlash against microservices stems from their frequent misuse as a default architecture rather than a deliberate choice for specific scaling needs. For most applications—especially early-stage products—starting with a well-structured modular monolith using clean architectural principles provides better velocity and maintainability.

The wisdom in your approach lies in recognizing that code organization and deployment strategy are separate concerns. You can structure your code for potential future distribution while maintaining the simplicity of a monolith deployment. This pragmatic approach avoids over-engineering while preserving architectural flexibility for when (and if) you truly need to scale.

Final Insight:

"Good architecture maximizes options, not complexity."

With GoFr, you get:

  • Simplicity when you need it
  • Scalability when you require it
  • Sanity throughout your journey

Explore GoFr to build systems that grow with your needs, not your headaches.