What Didn't Work: Lessons from Failures and Dead Ends

BusinessMath Quarterly Series

8 min read

Part 40 of 12-Week BusinessMath Series


Not everything worked. Some ideas seemed brilliant on paper but failed in practice. Some approaches worked technically but created more problems than they solved. Here’s the honest assessment of what didn’t work, why it failed, and what I’d do differently.


1. Over-Engineered Type System (v0.1 Mistake)

What I Tried: Create elaborate type hierarchy for financial instruments.

The Code:

// DON'T DO THIS
protocol FinancialInstrument {
    associatedtype CashFlowType
    associatedtype ValuationType
    func cashFlows() -> [CashFlowType]
    func value(discountRate: Double) -> ValuationType
}

protocol FixedIncomeInstrument: FinancialInstrument where CashFlowType == FixedCashFlow {
    var couponRate: Double { get }
    var maturity: Date { get }
}

protocol EquityInstrument: FinancialInstrument where CashFlowType == DividendCashFlow {
    var dividendPolicy: DividendPolicy { get }
}

// This went on for 15 protocols...

Why It Failed:

What I Learned: Start with structs. Add protocols only when you have 3+ implementations that share behavior.

The Fix:

// DO THIS INSTEAD
struct Bond {
    let faceValue: Double
    let couponRate: Double
    let maturity: Period

    func price(yield: Double) -> Double {
        // Simple implementation, no protocol maze
    }
}

struct Stock {
    let expectedReturn: Double
    let volatility: Double

    func expectedPrice(horizon: Period) -> Double {
        // Different from Bond, that's OK!
    }
}

Lesson: YAGNI applies to type systems too. You probably don’t need that protocol.


2. Premature GPU Optimization (Month 2 Fiasco)

What I Tried: “Everything should run on GPU for maximum performance!”

The Code:

// Tried to GPU-accelerate even simple operations
func npv(discountRate: Double, cashFlows: [Double]) -> Double {
    // Copy to GPU
    let gpuBuffer = device.makeBuffer(bytes: cashFlows, length: cashFlows.count * 8)

    // Run Metal compute shader
    let commandBuffer = commandQueue.makeCommandBuffer()!
    // ... 50 lines of Metal boilerplate ...

    // Copy result back
    return result
}

Why It Failed:

What I Learned: Optimize only after measuring. Profile first, then optimize the actual bottleneck.

The Fix:

// Simple CPU version (fast enough for 99% of use cases)
func npv
            
              (discountRate: T, cashFlows: [T]) -> T { cashFlows.enumerated().reduce(T.zero) { sum, pair in let (period, cashFlow) = pair return sum + cashFlow / T(1 + discountRate).pow(T(period + 1)) } } // GPU version ONLY for specific use case (genetic algorithm with 10,000+ population) if populationSize > 1_000 && Metal.isAvailable { return gpuGeneticAlgorithm(...) } 
            

Lesson: Default to simple. Add complexity only when profiling proves it’s needed.


3. Magical Auto-Constraint Detection (Abandoned Feature)

What I Tried: Optimizer that automatically infers constraints from domain.

The Idea:

// User writes this:
let portfolio = Portfolio(assets: 50)

// Optimizer "magically" knows:
// - Weights sum to 1 (it's a portfolio!)
// - No negative weights (you can't short!)
// - Max position size 20% (industry standard!)

let result = optimizer.optimize(portfolio)  // No constraints specified

Why It Failed:

What I Learned: Explicit is better than implicit. Always. No matter how “obvious” it seems.

The Fix:

// Make constraints explicit (even if verbose)
let result = optimizer.minimize(
    objective,
    constraints: [
        .sumToOne,      // User sees this is applied
        .longOnly,      // User sees this is applied
        .positionLimit(max: 0.20)  // User can change this
    ]
)

Lesson: Magic is good in demos, terrible in production. Be explicit.


4. Over-Abstracted Optimization Framework (Month 4 Rewrite)

What I Tried: “Let’s make it so generic you can optimize ANYTHING!”

The Code:

protocol OptimizationProblem {
    associatedtype Solution
    associatedtype Constraint: ConstraintProtocol
    func evaluate(_ solution: Solution) -> Double
    func constraints() -> [Constraint]
}

protocol Optimizer {
    associatedtype Problem: OptimizationProblem
    func solve(_ problem: Problem) -> Problem.Solution
}

// Now users have to implement 2 protocols + 3 associated types just to optimize

Why It Failed:

What I Learned: Abstractions should reduce complexity, not create it.

The Fix:

// Just use closures
func minimize
            
              ( _ objective: (T) -> Double, startingAt initial: T, constraints: [(T) -> Double] = [] ) -> T { // Simple, understandable, works } // Usage is obvious let result = optimizer.minimize( { weights in portfolio.variance(weights) }, startingAt: equalWeights, constraints: [sumToOne, longOnly] ) 
            

Lesson: The best abstraction is no abstraction. Closures are often enough.


5. Documentation Generation from Tests (Cool But Useless)

What I Tried: Auto-generate docs from test assertions.

The Code:

// Tests with special comments
func testNPVPositiveReturns() {
    /// @example NPV with positive returns
    /// @expectedResult Positive NPV indicates good investment
    let npv = npv(discountRate: 0.10, cashFlows: [-100, 30, 40, 50])
    XCTAssertGreaterThan(npv, 0)  /// @assert "NPV must be positive for profitable investment"
}

// Tool parses comments → generates docs

Why It Failed:

What I Learned: Just because you CAN automate something doesn’t mean you SHOULD.

The Fix:

/// Calculate NPV for a series of cash flows.
///
/// ## Example
///
/// ```swift
/// let npv = npv(discountRate: 0.10, cashFlows: [-100, 30, 40, 50])
/// // → 8.77 (positive NPV = good investment)
/// ```
public func npv
            
              (discountRate: T, cashFlows: [T]) -> T { // Manual docs, clear and concise } 
            

Lesson: Write docs manually. It’s faster and better.


6. Trying to Support Every Financial Standard (Scope Creep)

What I Tried: “Let’s support GAAP, IFRS, Japanese GAAP, German HGB…”

Why It Failed:

What I Learned: Pick one standard, do it well. Add others only when users demand it.

The Fix:

Lesson: You can’t be everything to everyone. Choose your battles.


7. Type-Level Dimensional Analysis (Compile-Time Units)

What I Tried: Make units (dollars, percentages, basis points) compile-time checked.

The Code:

struct USD: UnitType {}
struct Percentage: UnitType {}
struct BasisPoints: UnitType {}

struct Quantity
            
               { let value: Double } // Compiler prevents mixing units! let price = Quantity
              
                (value: 100.0) let return = Quantity
                
                  (value: 0.10) let x = price + return // ✗ Compile error: can't add USD + Percentage 
                
              
            

Why It Failed:

What I Learned: Type-level programming is fun but rarely worth the complexity tax.

The Fix: Use runtime validation for critical conversions, rely on clear naming for the rest.

// Simple, clear, works
func sharpeRatio(expectedReturn: Double, stdDev: Double) -> Double {
    expectedReturn / stdDev  // Clear from names what units are
}

Lesson: Types should clarify, not obscure. Fancy types are usually overkill.


The Meta-Lesson

Most failures came from the same root cause: Over-engineering.

I tried to be clever instead of simple. I tried to prevent every possible error instead of handling actual errors. I tried to support every use case instead of the common cases.

The pattern:

  1. Identify problem
  2. Design elaborate solution
  3. Implement for 2 weeks
  4. Realize it’s too complex
  5. Delete it all
  6. Write simple version in 2 hours
  7. Simple version is better

The real lesson: When in doubt, do less.**


Questions to Ask Before Adding Complexity

  1. Is this solving a real problem users have? (Not just “wouldn’t it be cool if…”)
  2. Can I solve this with existing features? (Closures > protocols, runtime checks > type gymnastics)
  3. Will this make the API simpler or harder? (If harder, probably skip it)
  4. Am I doing this because it’s fun or because it’s needed? (Be honest!)

Most features fail question #1. Be ruthless.


Tomorrow: “Final Statistics” — project metrics, test coverage, performance benchmarks, and what we actually shipped.


Series: [Week 12 of 12] | Topic: [Reflections] | Case Studies: [5/6 Complete]

Topics Covered: Over-engineering • Premature optimization • Magic abstraction • Scope creep • Type complexity • Simplicity wins

Final Week: [2 posts remaining] • [Final case study Thursday]


Tagged with: development-process