Why not do things the "best" way first?

I believe there is something to be said for doing things the "best" way right off the bat. From my experience in large FinTech companies, I tend to see lots of very verbose and/or "hacky" solutions for common engineering problems.

This observation has led me to question a fundamental assumption in software development: the idea that we should "get it working first, then optimize." While this approach has merit in certain contexts, I've seen it become an excuse for poor practices that compound over time.

The Cost of "Quick and Dirty"

In the high-pressure environment of financial technology, deadlines are king. Product managers breathe down your neck, stakeholders demand features yesterday, and "technical debt" becomes a euphemism for "problems we'll deal with later" (spoiler: we rarely do).

Here's a real example I encountered at a major investment firm. The task was simple: validate user input for a trading interface. The "quick" solution looked like this:

function validateTradeInput(input) {
  if (input && input.length > 0 && input.length < 100 && !input.includes('<') && !input.includes('>') && !input.includes('script') && !input.includes('alert') && parseFloat(input) && parseFloat(input) > 0 && parseFloat(input) < 1000000) {
    return true
  }
  return false
}

This function shipped to production. It "worked" in the sense that it prevented some basic issues, but it was a time bomb. No one could easily understand what it was validating, it was impossible to maintain, and it had subtle bugs that surfaced months later.

The "Best" Way Doesn't Mean Perfect

The best way isn't about writing perfect code—it's about writing code that's maintainable, testable, and understandable. Here's how that same validation function should have been written:

interface TradeValidationRules {
  minAmount: number
  maxAmount: number
  maxLength: number
  allowedPattern: RegExp
}

class TradeInputValidator {
  private rules: TradeValidationRules

  constructor(rules: TradeValidationRules) {
    this.rules = rules
  }

  validate(input: string): ValidationResult {
    const sanitizedInput = this.sanitizeInput(input)
    const errors: string[] = []

    if (!this.isValidLength(sanitizedInput)) {
      errors.push(`Input must be between 1 and ${this.rules.maxLength} characters`)
    }

    if (!this.isValidFormat(sanitizedInput)) {
      errors.push('Input contains invalid characters')
    }

    const amount = this.parseAmount(sanitizedInput)
    if (!this.isValidAmount(amount)) {
      errors.push(`Amount must be between $${this.rules.minAmount} and $${this.rules.maxAmount}`)
    }

    return {
      isValid: errors.length === 0,
      errors,
      sanitizedInput,
      parsedAmount: amount
    }
  }

  private sanitizeInput(input: string): string {
    return input.trim().replace(/[<>]/g, '')
  }

  private isValidLength(input: string): boolean {
    return input.length > 0 && input.length <= this.rules.maxLength
  }

  private isValidFormat(input: string): boolean {
    return this.rules.allowedPattern.test(input)
  }

  private parseAmount(input: string): number | null {
    const parsed = parseFloat(input)
    return isNaN(parsed) ? null : parsed
  }

  private isValidAmount(amount: number | null): boolean {
    return amount !== null && 
           amount >= this.rules.minAmount && 
           amount <= this.rules.maxAmount
  }
}

// Usage
const validator = new TradeInputValidator({
  minAmount: 0.01,
  maxAmount: 1000000,
  maxLength: 100,
  allowedPattern: /^[\d.,\s]+$/
})

const result = validator.validate(userInput)
if (!result.isValid) {
  displayErrors(result.errors)
}

Yes, this is more code. But consider the benefits:

  • Maintainable: Each method has a single responsibility
  • Testable: You can unit test each validation rule independently
  • Configurable: Different trading interfaces can use different rules
  • Debuggable: When something fails, you know exactly where and why
  • Extensible: Adding new validation rules doesn't require rewriting everything

The False Economy of Speed

The original "quick" solution took about 30 minutes to write. The robust solution took about 2 hours. In the short term, the quick solution wins.

But here's what happened over the next six months:

The Quick Solution:

  • 4 bug reports requiring hotfixes (3 hours each)
  • 2 feature requests that required complete rewrites (8 hours each)
  • 1 security vulnerability that required emergency patching (6 hours)
  • Countless hours of developer confusion and support tickets

Total time spent: ~30 hours

The Robust Solution:

  • 1 minor configuration change to adjust limits (15 minutes)
  • Easy addition of new validation rules as requirements evolved (1 hour total)

Total time spent: ~3 hours

The "fast" solution was actually 10x more expensive in the long run.

When Speed Actually Matters

I'm not advocating for over-engineering every piece of code. There are legitimate cases where quick and dirty makes sense:

Prototypes and POCs: When you're validating assumptions and the code will be thrown away, optimize for learning speed over code quality.

True emergencies: When the system is down and revenue is bleeding, fix first, refactor later.

Experiments: When you're A/B testing features that might not survive, don't invest in production-quality code yet.

But here's the key: be intentional about it. Make a conscious decision that you're accumulating technical debt, document why, and have a plan to address it.

The FinTech Reality Check

Working in financial technology has taught me that "good enough" often isn't good enough when:

  • Regulations are involved: The SEC doesn't care that you were in a hurry
  • Money is on the line: A bug that miscalculates trades isn't just embarrassing—it's expensive
  • Scale matters: A small inefficiency becomes a huge problem at millions of transactions per day
  • Teams are large: What makes sense to you at 2 AM won't make sense to your colleague in three months

Building a Culture of Quality

The real challenge isn't technical—it's cultural. How do you convince stakeholders that spending extra time upfront saves money overall?

Document the cost of poor decisions: Track how much time is spent fixing issues that could have been prevented with better initial implementation.

Make quality visible: Use metrics like code coverage, cyclomatic complexity, and technical debt measurements to quantify code health.

Educate stakeholders: Help product managers understand that technical debt isn't free—it has interest rates that compound over time.

Lead by example: Write good code consistently, and others will follow.

The Compound Interest of Good Code

Just as financial markets reward compound interest, codebases reward compound quality improvements. Every well-written function makes the next function easier to write. Every clear abstraction makes future features simpler to implement.

Consider this progression in a typical project:

Month 1: Good practices feel slow compared to quick hacks Month 6: The well-structured codebase starts moving faster than the hacky one Year 1: The quality difference is dramatic—new features take days instead of weeks Year 2: The technical debt in the hacky codebase has made it nearly unmaintainable

Practical Guidelines for "Best Way First"

Here are the practices I've found most effective:

Start with types: In TypeScript projects, define your interfaces and types first. They force you to think through the problem clearly.

Write tests early: Not necessarily TDD, but test-writing surfaces edge cases and design flaws quickly.

Prioritize readability: Code is read far more often than it's written. Optimize for the reader, not the writer.

Use tools consistently: Linters, formatters, and static analysis tools catch problems before they become bugs.

Document decisions: Not just what the code does, but why it was written that way.

The Long Game

Software development is a marathon, not a sprint. The developers who consistently deliver high-quality solutions aren't necessarily the fastest coders—they're the ones who avoid the pitfalls that slow everyone else down.

In my experience, the teams that insist on doing things the "best way first" consistently outperform teams that rely on quick fixes. They ship more reliable software, spend less time on maintenance, and have happier developers.

The best time to start building quality code was at the beginning of your project. The second-best time is now.

Conclusion

"Move fast and break things" might work for social media startups, but it's a dangerous philosophy in domains where reliability matters. Financial services, healthcare, infrastructure—these fields require a different approach.

Doing things the "best way first" isn't about perfectionism or gold-plating. It's about recognizing that the extra time spent thinking through a problem, designing a clean solution, and implementing it properly is an investment that pays compound returns.

The next time you're tempted to take a shortcut, ask yourself: am I saving time, or am I just moving the cost to someone else (including my future self)? The answer will guide you toward better decisions and better code.

In a world where software eats everything, the quality of that software matters more than ever. Why not make it good from the start?