← Dev Blog / Origin Story Series

Episode 02  ·  Origin Story Series

The Tech Stack — and the First Real Problem

Why I chose a "boring" stack over a fashionable one — and the scheduling algorithm that worked perfectly in my head and spectacularly failed in production.

Every developer building their own product faces the same first decision: what do I build it with? And hidden inside that question is a more honest one — what do I already know well enough to trust when things go wrong at midnight?

There's always a temptation to greenfield everything on the newest, most interesting stack. I felt it. But I've been in software long enough to know that when you're also the product manager, the league commissioner, the customer support team, and the one recruiting players out of DFW golf courses on weekends — the last thing you need is to be debugging framework issues you've never seen before at the same time.

So I went boring. Intentionally, deliberately, proudly boring.

"The best stack for a solo founder isn't the most exciting one. It's the one where you can read the error message at 1 a.m. and already know what it means."

— Brian Hackney, Founder

The Tech Stack

NTMGL runs on a stack I've used professionally for years across document processing systems and enterprise applications. Every piece was chosen for a reason: ASP.NET Core MVC (.NET 8) for server-rendered Razor views with no SPA complexity, Entity Framework Core for code-first migrations, SQL Server on Azure for relational data with real constraints, Microsoft Azure App Service for managed hosting, Bootstrap with custom CSS for a UI that works on a phone at the driving range, and vanilla JavaScript where interactivity is needed — no build pipeline required.

The choice that gets the most skeptical looks from developer friends is the server-rendered MVC approach. Why not React? Why not Next.js? Because I don't need three engineers and a CI/CD pipeline. I need a scorecard to load in two seconds on a phone at a golf course where the signal is questionable. Razor pages, a well-indexed SQL query, and a fast CDN handle that better than a hydrating React app pulling from a separate API endpoint ever will — at this scale, with one developer, on a budget that doesn't exist yet.

The Data Model

The database schema was where the real design work happened. The league's core concept — teams, seasons, scheduled matches, individual player matches within those team matchups, and hole-by-hole scores — sounds manageable until you sit down to model it.

Core Entity Relationships

The schema that runs a season

  • Season → has many Teams, defines the schedule window and handicap rules
  • Team → has a Captain, belongs to a Season, has many Players
  • ScheduledMatch → two Teams, a date, a course, and a status (Scheduled / In Progress / Complete)
  • IndividualMatch → two Players, belongs to a ScheduledMatch, produces a point total for each player
  • HoleScore → a single hole result within an IndividualMatch — the atomic unit of all scoring
  • PlayerHandicap → a differential that must be current at the time of the match, not just today's value

The handicap piece deserves its own mention. It's not enough to store a player's current handicap index — you need to record what it was when each match was played, because handicaps change after every round. Getting this wrong means your historical scorecards would recalculate differently over time. The fix is to snapshot the handicap at the moment a match is created, not pull it live when rendering the scorecard.

These are the kinds of domain-specific nuances that no tutorial covers. You only find them by building the thing and then asking yourself uncomfortable questions at 11 p.m. about data integrity.

The First Real Problem

The schedule generator was supposed to be straightforward. A double round-robin — every team plays every other team twice over the course of a season. I wrote the algorithm, tested it in my head against a four-team example, it looked correct, and I wired it up to the admin interface.

First time I ran it against the actual Season 1 team list, every matchup had been generated four times. Not twice — four times. Perfectly, consistently wrong.

The generator was iterating over all teams as "home" and pairing them with all other teams as "away." But I had also added a second pass to handle return fixtures. What I missed was that the base loop itself was already generating both directions — Team A vs Team B and Team B vs Team A — because I was iterating without a proper lower-triangle constraint. When the second pass ran, it doubled a schedule that was already doubled.

The Bug

Double Round-Robin Generated Quadruple

  • Base loop iterated all team pairs in both directions (A→B and B→A) without a lower-triangle constraint
  • Second pass for return fixtures ran on top of an already-doubled schedule
  • Result: every matchup appeared four times instead of twice
  • Fix: change j = 0 to j = i + 1 in the inner loop — six characters

The fix was a simple index constraint — only generate pairs where the inner loop starts at i + 1, then generate return fixtures as a separate explicit pass. Six characters. One hour of debugging. A unit test that now runs in 40 milliseconds and will never let it happen again.

"Six characters. One hour of debugging. A unit test that now runs in 40 milliseconds and will never let it happen again."

— The actual cost of the first real bug

What I Learned

The stack decision was the right one. Every time something breaks — and things break constantly when you're building fast — I'm reading familiar error messages in a familiar framework. EF Core exceptions, ASP.NET model binding issues, Azure SQL query plans — these are things I've seen before. That familiarity is worth far more than the novelty of a stack I'd be learning from scratch under pressure.

The scheduling bug taught me something less about code and more about scope. When you're the only developer, every assumption you don't test becomes a bug someone else finds. The discipline of writing at least a smoke test for anything that touches the core data model before you ship it is non-negotiable.

Season 1 launched with a correct schedule, correctly generated, correctly verified. Forty-five matches played. Every one of them counts.


Next episode: building the scoring engine — the most complex piece of the platform, and the one that had to work perfectly the first time players walked onto a course with their phones.

Tags tech stack architecture debugging scheduling