Product design · 2026
Partner matching for pickleball doubles, built spec-first. A working prototype of the matching model, and a written record of every design decision behind it — including the ones I got wrong and had to revise.
I compete in pickleball tournaments at the 5.0 level, so this is a problem I run into myself. The sport is growing fast and there is no shortage of players. What there is a shortage of is signal.
You cannot tell who is currently looking for a partner. Asking someone implies you want to replace the partner they already have, so nobody asks. And fit gets guessed from a single skill rating, which is why two players of identical level can be a terrible team. Doubles is a role sport: two bangers with nobody crashing the net lose to a drop-and-grind pair every time.
The scarce thing isn't players. It's knowing who is open, and whose game fills the gaps in yours.
The existing way people find partners is open play — rotate through dozens of players over months and gradually work out who you click with. It works, and I didn't want to replace it. Instead I scoped the product to the two groups it fails completely.
Registered for an event in a city where they know nobody. High intent, hard deadline, willing to risk a stranger because the alternative is not playing.
Wants to get better but doesn't attend open play — intimidating, wrong schedule, or they want structured drilling rather than rotating rec games.
These two want nearly opposite things from the same ranking. That single observation forced the biggest structural decision in the product: the scoring weights are selected by intent rather than shared. Schedule overlap is the heaviest term for a recurring local partner and undefined for a one-weekend tournament team.
The obvious approach is to score people by how alike they are. That is wrong here, and wrong in a way that produces confidently bad matches. Fields fall into three categories, and each needs different maths.
A similarity score gets the first row right, the second exactly backwards, and cannot express the third at all.
I also chose named archetypes over a black-box distance function — Bang & Crash, Two Grinders, and anti-patterns like Collision Course for two players who both poach. It is slightly less precise, and it means every match explains itself in one line. An explainable 80 beats an inscrutable 95: people trust a score they can argue with, and the explanation teaches them the sport, which is its own reason to come back.
My first draft assumed something that sounds obvious: everyone wants a partner rated above them, and nobody wants one rated below. I built the whole acceptance model on that asymmetry. It's wrong.
DUPR — the sport's rating system — sets an expected score from the average of the two partners' ratings. Your rating then moves on the gap between the actual result and that expectation, which means you can win a match and lose rating.
A much higher-rated partner raises the bar you have to clear. Meeting expectation earns you almost nothing; falling short costs you.
So a player optimising to climb should decline a partner far above them. The rating-optimal partner isn't the strongest available — it's an underrated one, whose rating sits at or slightly below yours but whose actual play exceeds it.
The fix was to stop treating preference direction as universal and derive the acceptance band from what each player is actually optimising for — winning the event, climbing, improving, or just having a good time. Two players optimising differently can now match with each other, which the original model would have filtered apart.
The second half of the fix was rating stakes. The entire problem only exists because results get uploaded. Letting a session be declared unrated costs the stronger player nothing and unlocks the cross-level play that Persona B needs and the rated market structurally suppresses. In the prototype, toggling it takes the candidate pool from 11 players to 18.
Every non-obvious decision goes in a register with its rejected alternative, its reasoning, its cost, and the condition that should make me reopen it. Twenty-six entries so far. A decision with no downside listed usually means I hadn't actually examined it.
The prototype runs the real matching model against sixty seeded players. It exists to answer one question — do the matches feel right? — and it surfaced two things the written spec could not.
A weight can be live in the config and dead in practice. Schedule overlap was the heaviest factor for local matching, defined as needing four shared hours a week. But players only list four to six hours of availability in total, so demanding four shared hours demanded near-total alignment. The factor collapsed to roughly zero for almost every pair, quietly nullifying the most important term — while the overall score still looked completely plausible. Now I check the distribution of each component, not just the final number.
Filters compound much harder than any one of them looks. Of sixty players, only nineteen survived the default view. Each filter was individually defensible; stacked together they removed two-thirds of an already-small pool. That became its own open entry in the register — intent is probably better as a ranking penalty than an elimination, because someone who lists "drilling" may well say yes to a casual game.
Sixty seeded players, the full matching model, and an explanation attached to every score. Set your objective to “climb” and watch the strong partners disappear.