What I Optimize for as a Builder

Dhruval Dhameliya·June 11, 2025·7 min read

The specific qualities I prioritize when designing and building software systems, and why these priorities differ from what most engineering culture emphasizes.

Context

Every builder optimizes for something, whether consciously or not. Some optimize for performance. Some for elegance. Some for shipping speed. The choices you make about what to prioritize shape the system in ways that are difficult to reverse.

Over the years, my priorities have shifted significantly. What I optimize for now is different from what I optimized for five or ten years ago. The shift was driven by maintaining systems over time and watching which qualities made that maintenance sustainable and which made it painful.

What I Optimize For

1. Changeability Over Perfection

The first version of a system is the least informed version. You know the least about the problem, the users, and the constraints at the beginning. Designing for perfection up front locks in assumptions that will be wrong.

Related: Designing Event Schemas That Survive Product Changes.

See also: Designing a Feature Flag and Remote Config System.

I optimize for the ability to change the system later, when I know more. This means:

  • Small, focused modules with clear interfaces
  • Abstractions at boundaries, not everywhere
  • Data models that can evolve without breaking consumers
  • Deployment pipelines that make shipping changes fast and safe

A system that is easy to change will converge on the right design over time. A system that is designed for perfection from day one will resist the changes that would make it better.

2. Debuggability Over Cleverness

Clever code impresses during code review. Debuggable code saves you at 3 AM when production is on fire and you are trying to figure out what went wrong.

Debuggable systems have these properties:

  • Predictable behavior: Given the same inputs, the system does the same thing. No hidden state, no order-dependent side effects.
  • Observable state transitions: Every significant state change is logged with enough context to reconstruct the decision.
  • Clear error messages: Error messages that tell you what went wrong, what the system expected, and what it got instead.
  • Simple control flow: Linear, top-to-bottom execution paths that you can follow without a debugger.

I have seen systems where the original author's cleverness made the code 20% shorter but made debugging 5x harder. That is a bad trade.

3. Operability Over Feature Richness

A system with 100 features and no monitoring is less valuable than a system with 10 features and comprehensive observability, health checks, and automated recovery.

What operability looks like in practice:

  • Dashboards that show system health at a glance
  • Alerts that fire on user-impacting conditions, not on resource thresholds
  • Runbooks for common failure scenarios
  • The ability to increase or decrease capacity without downtime
  • The ability to roll back any deployment in under a minute
  • Log access and metric access without SSH

I budget 20-30% of development time for operational concerns. This is not overhead. This is the difference between a system that runs in production and a system that survives in production.

4. Boring Technology Over Exciting Technology

Boring technology has known failure modes, established operational practices, available expertise, and battle-tested libraries. Exciting technology has none of these.

Every exciting technology in the stack is a bet that the benefits will outweigh the learning curve, the unknown failure modes, and the operational gaps. Sometimes that bet pays off. More often, the exciting technology solves a problem you could have solved with a boring technology and adds operational risk you did not need.

My rule: one or two innovative technologies per system. Everything else should be boring. The innovative choices should be in the areas where they provide genuine differentiation, not in the database, the message queue, or the deployment pipeline.

5. Explicit Behavior Over Implicit Behavior

Implicit behavior hides the system's true nature. Convention-over-configuration frameworks, magic annotations, auto-wired dependencies: these all reduce the amount of code you write but also reduce the visibility of what the system actually does.

I prefer systems where the behavior is explicit and traceable. If I can read the code from the entry point and follow the execution path without looking up framework conventions, that is explicit. If I need to know that a specific annotation triggers a specific interceptor that modifies the request before it reaches my handler, that is implicit.

Explicit code is longer. It is also more debuggable, more testable, and more accessible to engineers who are not familiar with the specific framework's conventions.

6. Reversibility Over Commitment

Some decisions are easy to reverse: a feature flag, a configuration change, a library upgrade. Others are hard to reverse: a database schema, a public API, a programming language choice.

I spend proportional effort on decisions based on their reversibility. Easy-to-reverse decisions get quick judgment calls. Hard-to-reverse decisions get design documents, prototypes, and reviews.

Decision TypeReversibilityEffort I Invest
Feature flag toggleTrivialMinimal
Library choice (internal)ModerateBrief evaluation
API contract (public)Very hardDesign doc, review
Database schemaHardMigration plan, compatibility analysis
Programming languageNear-impossibleTeam alignment, long-term evaluation
Cloud providerVery hardMulti-year commitment analysis

What I Used to Optimize For

Early in my career, I optimized for:

  • Performance: Making things fast before I knew if they needed to be fast.
  • Elegance: Clean abstractions even when the requirements were unclear.
  • Comprehensiveness: Building for every possible future scenario.
  • Novelty: Using new technologies because they were new.

These priorities produced systems that were fast but brittle, elegant but rigid, comprehensive but unmaintainable, and novel but operationally challenging.

Key Takeaways

  • Optimize for changeability. The first version of a system is the least informed version. Make it easy to evolve.
  • Optimize for debuggability. Clever code trades readability for brevity. Debuggable code trades brevity for clarity. Choose clarity.
  • Budget 20-30% of development time for operability. This is an investment, not overhead.
  • Use boring technology for most of the stack. Save innovative choices for areas of genuine differentiation.
  • Prefer explicit behavior over implicit behavior. Explicit code is longer but more debuggable and more accessible.
  • Invest effort proportional to reversibility. Spend minutes on reversible decisions and weeks on irreversible ones.

Further Reading

Final Thoughts

What you optimize for as a builder reveals your mental model of software engineering. If you optimize for performance, you see software as a machine. If you optimize for elegance, you see it as craft. If you optimize for changeability and operability, you see it as a living system that will evolve and require care over its lifetime. I have found the last perspective to be the most accurate and the most useful.

Recommended