From Writing Perfect Code to Making Imperfect (But Right) Decisions

By Juan Villarroel Published on 5 min read

From Writing Perfect Code to Making Imperfect (But Right) Decisions
Architecture Product Clean Code

Quick answer: Maturity as a developer is not about writing more complex code, but about making better decisions. This article discusses avoiding overengineering, prioritizing real value, and building products with users in mind—not technical ego.

The silent trap of technical confidence

There comes a point in almost every developer's career when the problem stops being technical and becomes a matter of judgment.

You're no longer struggling with how to write a loop, how to connect to a database, or how to spin up a server. You've already mastered that. Now the enemy is something else—much more uncomfortable:

Making decisions.

The decisions that make a product work in the real world don't always match the decisions that make the code look elegant, modern, or intellectually satisfying.

This post is about that transition.

About moving from simply writing code to building things that truly matter.


Choosing what is best for the business

There's a question that carries more weight than any bug:

"Does anyone actually need this feature I want to build?"

Because let's be honest, we've all had:

  • That feature that sounds awesome.
  • That advanced dashboard with a thousand filters.
  • That complex system that “would look epic”.

… but that nobody asked for. Nobody validated. Nobody is waiting for.

Learning to say:

"This is great, but it's not what delivers the most value right now"

is one of the biggest leaps in maturity as a developer.

It is not about limiting creativity. It is about putting it in service of real impact, not technical ego.


The cost of overcomplicating too early

At some point we discover microservices, Kubernetes, event-driven architectures, and CQRS, and we want to use them all.

The problem is when you do it in a project that hasn't even proven it needs to scale yet.

Then problems start to appear:

  • Logs scattered across 5 services.
  • Debug sessions where you don't even know where to begin.
  • Crossed dependencies that are difficult to trace.
  • Incidents whose origin is not obvious.

Sometimes all you need is a well-organized monolith and a database to validate your idea.

Scaling is fine; scaling without a real need is overengineering.


Benchmarks do not replace context

A framework may handle hundreds of thousands of requests per second, but that number matters little if the API receives a few dozen.

So many times we choose technology based on extreme benchmarks, when the real problem is somewhere else:

  • Queries without indexes.
  • N+1 queries.
  • Fetching 10,000 rows when you only needed 20.
  • No caching at all.
  • Nonexistent pagination.

Switching frameworks won't fix a query that takes 2 seconds because you're pulling half the database.

Most slow APIs do not fail because of the router; they fail because of poorly designed data access.


Sometimes a docker compose up is enough

Not every project needs:

  • A distributed cluster
  • Autoscaling
  • A service mesh
  • 4 environments per microservice

Sometimes you just need:

  • A backend
  • A database
  • A Redis instance

And for everything to start predictably.

Simple means understandable; understandable means maintainable; maintainable means fewer operational incidents.

Simplicity is not mediocrity. It is often experience applied.


The false microservice: the distributed monolith

This is a common pattern because many teams have been there.

"We have microservices", but they all use the same database.

Result:

  • A change in one table breaks half the system.
  • Everything depends on everything.
  • Nobody can deploy without coordinating with half the team.

That is not microservices; it is a monolith with network problems.

If you're going to split things, split them for real:

  • Clear boundaries.
  • Data independence.
  • Communication through events or well-defined APIs.
  • Fault tolerance between modules.

If one service goes down and takes the whole system with it… it wasn't really independent to begin with.


The obsession with perfect code

We've all spent hours:

  • Renaming variables.
  • Reorganizing folders.
  • Refactoring without an observable need.
  • Improving an architecture nobody else sees.

Meanwhile, the feature:

  • Hasn't been validated.
  • Nobody asked for it.
  • Nobody uses it.

Validate first; then improve it.

Refactoring something that is actually used is an investment. Perfecting something nobody wants wastes time and attention.


Tests: an early investment that reduces risk

Adding tests to an already massive project is like trying to install seat belts after the crash.

Yes, writing tests from the beginning takes time. But adding them when the system is already large, fragile, and full of patches costs far more.

It's always better than:

  • Breaking something without realizing it.
  • Being afraid to refactor.
  • Not knowing whether your change broke something else.

Tests are not there to impress with coverage. They provide confidence as the codebase grows and needs to change.


Do not commit to a technology by default

We all have a:

  • Favorite language.
  • Favorite framework.
  • Favorite stack.

And that is fine. What is dangerous is thinking that is the solution to everything.

Sometimes we use a technology because:

"It's the best thing in the world"

But in reality: It's the most complex thing in the world for a simple problem.

For that specific problem, it may be a disproportionate solution.

It is not black and white; it is context.

Choosing well is not about following trends. It is about understanding what I need, what I know how to use, and what lets me move forward without unnecessary complexity.


Code Less, Decide Better

Over time, you come to understand something key:

Being a good developer is not just about knowing how to build complex things. It is about knowing when not to build them.

Technical maturity isn't reflected in how many tools you use, but in how many problems you avoid creating.

Sometimes growth isn't about adding more layers… it's about removing the ones that were never needed.

And that, even if it doesn't always look spectacular on GitHub, is what truly builds products that last.

Applying the principle to concrete decisions

The idea becomes useful when it stops being a quote and changes a choice. In backend work, that can mean starting with a monolith organized by vertical slices and evaluating NestJS from real needs without copying every available pattern.

In frontend work, it can mean learning from hands-on experience with Astro without assuming the same framework fits every product. The common pattern is documenting context, accepted risk, and the signal that would justify revisiting the decision.

Related Posts