Knowing How to Build It Is Different From Knowing If You Should
· 7 min
"I learned that knowing how to build something complicated is different from knowing when something complicated is necessary."
Early in my career I associated good engineering with modern tools, scalable architecture, and well-designed abstractions.
Whenever I learned a new framework or infrastructure tool, I wanted to find a way to use it. A more sophisticated setup felt like proof that I was building software the right way. If a system was designed to scale and built on a modern stack, it had to be better. At least, that was how I saw it.
That did not come from a bad place. I wanted to learn, and I wanted to prove I could build more than simple applications.
But working on a real project with a client taught me something:
A technically capable solution is not always the same as the right solution.
Sometimes the difficult part is not knowing how to build something. It is knowing whether the project actually needs to be built that way.
Our First Version
We designed the first version as a monorepo:
- Next.js for the frontend
- NestJS for the backend
- Supabase for the database
- Cloudflare R2 for object and file storage
Every choice was defensible. The monorepo kept related applications in one repository. NestJS gave the backend structure as the business logic grew. Supabase gave us managed Postgres without anyone having to operate it. R2 handled files.
None of it was wrong, and the lesson was not that we should have avoided any of it.
The lesson was this:
We were making important architectural decisions before we understood the shape of the product.
At the start of a project it is easy to focus on the stack, the repository layout, the storage provider, and how the system will scale. Those questions are useful. They just come second.
What does the client actually need?
Requirements Are Engineering Work
Understanding requirements is not a step before development. It is part of engineering.
A client might say:
"We need users to upload files."
That sounds simple. Then you ask who can upload, what types and sizes are allowed, who can read them, whether the files are private, whether they can be replaced, and whether storage is core to the product or incidental to it.
The answers decide the database design, the API structure, the storage solution, and the security model.
The same goes for the questions that sound like business questions rather than technical ones. Who the users are, what the first version must accomplish, which features are required for launch, what the timeline and budget are, and what success looks like to the client. Every one of them constrains the architecture.
Without that context, developers fill the gaps with assumptions.
And assumptions quietly become architecture.
When Requirements Are Unclear, Assumptions Become Architecture
When we do not fully understand the product, we start designing for possibilities instead of confirmed requirements.
"We might need this later."
"What if the client asks for it?"
Thinking ahead is not bad, and planning for growth is part of good engineering. The problem begins when every possible future is treated as a requirement for the present.
That is how you end up building features nobody requested, abstractions around workflows that later change, and infrastructure for scale the project may not reach for years, if ever.
We were not building for the product. We were building for a future we imagined.
The architecture should follow the product, not assumptions about what the product might become.
A system does not need to support every use case on day one. It needs to solve the current problem reliably while staying understandable.
The Hidden Cost of Overengineering
Overengineering is not only about having too many technologies. Every layer costs time: researching it, configuring it, learning it, debugging how it interacts with everything else, maintaining it, documenting it, and onboarding the next developer into it.
A tool might take an hour to add. Understanding, debugging, and eventually changing it costs much more.
In freelance or client work this matters, because a fixed budget means extra hours are not extra payment.
Imagine accepting a project for ₱50,000, estimated at 100 hours:
₱50,000 ÷ 100 hours = ₱500 per hour
Now suppose unfamiliar tools and unexpected integration problems push it to 180 hours:
₱50,000 ÷ 180 hours = approximately ₱278 per hour
The project fee did not change. The value of your time did.
That is why overengineering stops being only a technical issue. It reaches profitability, delivery timelines, client expectations, and the hours you had available for other work.
Complexity Should Be Earned
Complexity is not the enemy. A monorepo is not automatically overengineering, and Supabase and R2 are not automatically unnecessary. A monorepo helps when multiple applications share code. A structured backend earns its place when the business logic is complex. Managed infrastructure is worth it when nobody on the team wants to operate the alternative.
The question should not be:
"Is this technology advanced?"
The better question is:
"What real problem does this technology solve for this project?"
If the answer is clear and the benefit justifies the cost, the complexity is worth it. If the answer is "we might need it someday," it is worth reconsidering.
Complexity should be earned.
A system should become more complex because its requirements demand it, not simply because we know how to make it more complex.
Build for Today, Leave Room for Tomorrow
Requirements change. Clients learn more about their users, priorities shift, and features that seemed important turn out not to be.
We cannot predict all of it, and trying to leads to building too much too early.
The alternative is not ignoring the future. It is building today's requirements cleanly while avoiding decisions that make future changes expensive: keeping the code understandable, keeping boundaries clear, avoiding unnecessary coupling, and improving the architecture when a new requirement is real rather than imagined.
The goal is to avoid paying for the future before it arrives.
Questions I Ask Before Adding Complexity
- What specific problem does this solve, and does that problem exist today?
- Is the simpler solution actually insufficient?
- How much development and maintenance time does this add?
- Does the client need this in the first version?
- Can I clearly explain why this technology is necessary?
- Am I solving a confirmed requirement, or preparing for a hypothetical future?
- Am I choosing this because the project needs it, or because I want to use the technology?
The last one matters most.
Wanting to learn a new tool is a good thing, and experimentation is how we grow as engineers. But a client project is not always the right place to introduce a technology because we are curious about it. Usually the better approach is to learn it in a side project and bring it into client work when there is a clear reason.
What Client Work Taught Me
Engineering maturity is not measured by how much complexity we can introduce. It is measured by understanding the problem, recognizing the project's constraints, making reasonable tradeoffs, communicating with the client, and delivering what is actually needed.
Requirements are not a checklist developers receive before writing code. They influence the architecture, the timeline, the cost, and the amount of complexity a project actually needs.
The more clearly we understand what the client wants at the beginning, the fewer assumptions we build around, and the less time we spend rewriting work later.
Today I still value scalable architecture, modern tools, and thoughtful engineering. I just start somewhere different. Before asking what technologies we should use, I ask what problem we are solving and what the client actually needs.
Because the best solution is not always the one with the most tools. Sometimes the best engineering decision is choosing not to add another layer.