Back to Case Studies

Over-engineering a Clean Architecture

Designed a strict layered architecture in Go to prevent chaos—then learned to simplify it when structure became friction.

Context

This case involved the parallel development of two applications: a Warehouse Management System (WMS) and a Fleet Management system (FMS). Both were extracted from the Twin v1 monolith and rebuilt as standalone SaaS applications after the company separated those business units.
As the Senior Developer leading the architectural design, I defined the base modules, structural patterns, and overall code flow. My primary goal was to prevent a return to the architectural chaos of v1, where business logic was scattered across controllers without clear boundaries.

The Problem

The challenge was not performance or system instability, but architectural complexity. Since the majority of the team was experienced in PHP/Laravel but new to Go , I implemented a Technical Layering (Layered-by-Type) approach to provide a familiar mental model for the team.
I established a strict separation between validation, handlers, business logic, repositories, and response formatting. In practice, even though those strict principles created clean boundaries, they led to increased friction. While the structure was theoretically clean, Go lacks the framework abstractions of Laravel, meaning every layer required explicit, repetitive implementation.
We hit a specific wall with domain logic: sometimes there were crucial complex checkers that required deeper service layers to function. To align with the architectural principles, we often ended up duplicating validation logic across layers. This resulted in redundant database queries and unnecessary code complexity just to satisfy the rules of the structure.

Impact

This "Boilerplate Tax " made feature development significantly slower than necessary. With a limited team responsible for maintaining multiple SaaS applications, this overhead became a critical bottleneck.
The issue wasn't a technical failure, but an architectural overinvestment. The complexity of the structure didn't match the actual needs of the products, making it difficult to manage the sustainability of multiple platforms simultaneously while under constant delivery pressure.

Decision and Approach

Recognizing that the architecture had become a hindrance, I led a pivot toward a more Pragmatic Layering model. We chose to simplify the layers where structure had turned into friction, focusing on these key changes:
  • Reduced unnecessary layering where it added little value
  • Allowed validation logic in deeper layers when appropriate
  • Minimized duplicated database checks and redundant queries
  • Preserved clear boundaries while improving development flow

Outcome

Simplifying the layers significantly improved development speed and reduced the amount of manual boilerplate code. The system remained maintainable and testable, but with far less rigidity. The architecture became supportive rather than restrictive, allowing our limited team to manage the various SaaS applications more effectively and comfortably.

Reflection

At the time, I believed a strict structure was the only way to prevent technical debt and correct the weaknesses of v1. However, I learned that over-engineering can be just as harmful as under-engineering. I now prioritize Progressive Complexity : start simple, define clear boundaries, and only evolve the structure as real needs emerge. Architecture should serve the product—not the architect’s vision of perfection. Good ideas do not need to be implemented all at once; they are better introduced when the system truly demands them.

Questions & Answers

Q.
How did you identify which layers were "unnecessary" versus which were "essential"?

A.
We identified friction by looking for Functional Redundancy . For instance, in our stock-subtraction logic, we had a validation layer checking if stock existed, and a service layer that also had to 'catch and try' the same check during execution. We realized we were managing the same business rule in two places for two different reasons. By collapsing these, we didn't just remove code—we removed the risk of the validation and service layers ever falling out of sync.

Q.
By moving validation deeper into the service layers, didn't you risk "leaking" concerns and making unit testing harder?

A.
Every architectural choice is a trade-off. While moving validation deeper technically 'leaks' concerns into the service layer, it was a necessary pivot for our system's scale. In a massive enterprise app, strict separation is a shield; in a leaner SaaS, it’s a hurdle. We consolidated our test suites so that the service layer tests handled both the business invariant and the validation, ensuring we didn't lose coverage, just complexity.

You might also like my other articles

© 2026 — This site documents my work and thinking around software system.

Open to senior full-stack web engineering roles — [email protected]Privacy Policy