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

Key Architectural Decisions

1. Domain-Centric Organization

This structure organizes code by business domains (order, catalogue, customer) rather than by technical layers (controllers, services, repositories). Each domain contains its own complete vertical slice of the architecture.

Benefits:

  • Better alignment with business capabilities
  • Reduced cross-domain coupling
  • Easier to extract into microservices later if needed
  • More cohesive code organization

2. Self-Contained Modules

Each domain module contains:

  • Handler: HTTP/API delivery layer
  • Service: Business logic layer
  • Store: Data access layer

This follows the principles where:

  • Dependencies flow inward (handlers depend on services, services depend on stores)
  • Outer layers can be swapped without changing inner layers
  • Business logic remains agnostic

3. Flexible Deployment Options

The structure supports multiple deployment strategies:

Monolithic Mode:

  • Single main.go orchestrates all domains
  • Unified configuration
  • Single process monitoring

Microservice Mode:

  • Any service can be built and deployed separately using its own main.go and Dockerfile

Advantages Over Traditional Three-Layer

  1. Better Domain Isolation: Clear boundaries prevent accidental coupling
  2. Easier Testing: Each domain can be tested in isolation
  3. Progressive Complexity: Start simple, add distribution only when needed
  4. Team Scalability: Different teams can own different domains

When This Works Best

This architecture shines when:

  • You want to avoid microservices overhead but maintain separation
  • Your team is small-to-medium sized
  • You might need microservices eventually but not yet
  • Different domains have different evolution speeds

The key insight is that code organization doesn't have to match deployment topology. You can maintain clean separation in code while deploying as a monolith, then change deployment strategy later without massive rewrites.

Clean Three-Layer Architecture in Practice

1. Delivery Layer (Handler)

  • Receives HTTP requests
  • Handles:
    • Request validation
    • Authentication/authorization
    • Error formatting
    • Response marshaling
  • Should be thin - delegates business logic to service layer
  • 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.

  • Contains domain logic
  • Orchestrates multiple store operations
  • Enforces business rules

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
  }

3. Store Layer (Data Access)

  • Handles database operations
  • Manages:
    • Database connections
    • Query building
    • Caching

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.