Early in my journey as a software engineer, I became fascinated with software architecture.
I spent hours learning about:
- SOLID Principles
- Clean Architecture
- Hexagonal Architecture
- Ports and Adapters
- Dependency Inversion
The more I learned, the more I became convinced that every project should be highly decoupled, framework-agnostic, and future-proof.
Then I built a shopping cart application.
And that's when reality taught me a lesson that no book could.
Building It "The Right Way"
Instead of creating a straightforward React application, I decided to apply a strict layered architecture.
My project was structured like this:
UI
│
Controllers
│
Application
│
Domain
│
Adapters
│
Infrastructure
Every responsibility had its own layer.
Every dependency pointed inward.
Every interaction crossed carefully designed boundaries.
At first, I loved it.
The folder structure looked professional.
The separation of concerns looked clean.
The architecture looked impressive.
Then I started building features.
The Architectural Tax
A simple feature change often required updates across multiple layers.
Something that should have taken minutes turned into a journey through several directories and files.
I found myself spending more time maintaining architectural boundaries than solving actual problems.
That's when I realized something important:
Architecture isn't free.
Every layer comes with a cost.
Every abstraction comes with a cost.
Every boundary comes with a cost.
You pay for it with:
- More files
- More indirection
- More cognitive load
- More debugging effort
- Slower development velocity
The architecture wasn't wrong.
The problem was that the complexity of the architecture was greater than the complexity of the application itself.
Architecture Is a Budget, Not a Rulebook
This realization completely changed how I think about software design.
The question is not:
"Is this architecture clean?"
The better question is:
"What problem is this architecture solving, and is that problem large enough to justify its cost?"
A shopping cart application with a handful of features does not face the same challenges as a large enterprise system.
Treating both projects the same way can actually make the smaller project harder to maintain.
The Framework-Agnostic Illusion
One of my goals was to make the application completely independent of React.
I wanted to be able to swap React for another framework in the future.
In theory, that sounded like great engineering.
In practice, I realized I was optimizing for a scenario that might never happen.
The changes that are almost guaranteed to happen are:
- New features
- Requirement changes
- UI updates
- Bug fixes
- Refactoring
A complete framework migration is usually much less likely.
Today, I still keep business logic isolated, but I no longer try to abstract every single framework detail away.
What I Prefer Now
My current frontend architecture is much simpler:
Domain
Pure business logic.
No React.
No API calls.
No UI concerns.
Infrastructure
External systems such as:
- API clients
- Storage
- Third-party services
Hooks
State management and orchestration.
The bridge between the domain and the UI.
UI
Presentation components.
Focused on rendering and user interaction.
Nothing more.
This gives me most of the benefits I care about without introducing unnecessary complexity.
The Biggest Lesson
The lesson wasn't that Hexagonal Architecture is bad.
The lesson wasn't that Clean Architecture is wrong.
The lesson was this:
Architecture should solve real problems, not hypothetical ones.
Sometimes adding a boundary is the right decision.
Sometimes removing a boundary is the right decision.
The difficult part is knowing the difference.
And that's not something you learn from reading alone.
You learn it by building software, experiencing the friction, and understanding the trade-offs firsthand.
Final Thoughts
Engineering maturity isn't about applying every design pattern you've learned.
It's about understanding the cost and value of each decision.
The goal isn't perfect architecture.
The goal is reducing the cost of future change.
Build.
Ship.
Learn.
Then simplify.
Have you ever realized you were over-engineering a project? What lesson did it teach you?
Top comments (4)
The perfect-architecture trap is one of the most expensive habits good engineers have, because it disguises procrastination as diligence - you feel productive designing the flawless abstraction while shipping nothing. A shopping cart is the perfect teacher here: it's tempting to build for infinite scale and every future feature, when what you needed was a working cart you could ship and evolve. Premature abstraction is just speculative complexity you pay for now against a future that usually never arrives.
The balance I've landed on: build the simplest thing that solves today's real problem, with clean enough boundaries that you CAN evolve it - not the perfect thing for an imagined tomorrow. YAGNI plus sane seams. That ship-then-evolve bias is baked into how I think about Moonshift (a multi-agent pipeline that ships a prompt to a deployed SaaS) - get a real, working, deployed thing out, with sane defaults, rather than an over-architected masterpiece that never ships, ~$3 a build. Great lesson, well-told via the cart. What finally snapped you out of the perfect-architecture spiral - a deadline, or just shipping the simple version and seeing it was fine? That realization moment is the useful part.
This is exactly the tension I ran into.
What snapped me out of it wasn’t a deadline—it was friction while building.
I kept noticing that “small” changes were becoming multi-layer edits. At some point I realized I was spending more time preserving architecture boundaries than actually shipping value.
The turning point was when I stripped things down to a simpler structure and saw I could still evolve the system easily, just without all the overhead.
That’s when it clicked: I wasn’t building a better system, I was just building more structure than the problem needed.
YAGNI + clean seams is exactly the balance I’ve been leaning toward since then.
The perfect-architecture trap is one of the most expensive habits in software, you build the cathedral for a shopping cart that needed a lean-to. The lesson usually lands the hard way: architecture should match the problem's actual complexity and likely change, not your aspiration of what it might one day become. Over-engineering early is just speculative work that slows you down on the way to a real answer. I bias hard toward the simplest thing that ships and survives in Moonshift, then let real usage justify any added complexity. What pulled you toward the over-built version first, fear of future rework or just enjoying the design?
Congratulations, you have just graduated as an architect. The true role of an architect is understanding the architectural options and choosing the best fit for the business problem at that point in time. You’ll need to take into account everything from up front cost, security, resilience, maintainability, and even extend to what people resources you have available to build and then to operate and maintain. And a bunch more things too. It’s all about balancing different trade offs.
Some comments may only be visible to logged-in visitors. Sign in to view all comments.