On April 10, a ticket started as a prototype problem. Tournament event pricing for the production app: how should a team registration add cost to a tournament? I figured the design work was the hard part and the implementation would follow. By end of day the design was the only part that had gone to plan.
I ended up pushing that model through hybrid checkout, feature gates, proration math, unpaid-state handling, and architecture docs spread across multiple separate codebases. The prototype had been easy. The real thing was something else.
two layers, not one
The first instinct for tournament pricing was flat per-team pricing. Each team that enters pays X dollars. Clean, predictable, easy to explain to a tournament director. The second instinct was credits: pre-purchased blocks that deduct on each registration.
Both lost for the same reason. Neither maps cleanly onto what the production app already sells.
Every league and tournament on the platform is run by an organization that already has a membership tier. That membership tier controls what the organization can do. Adding event pricing on top of a flat-per-team model means the software has two separate concepts of “what this org paid for” running side by side, with no formal relationship between them.
The model I selected used two layers. The base layer remained the existing membership, with its price and access surface. The second layer was a bundle ceiling: an event could define how many team registrations were included in the membership cost, with additional registrations priced separately.
That structure fit this product because it connected the existing membership model to the way event capacity was configured. It was a product decision for this platform, not a universal pricing pattern.
the prototype was allowed to ignore proration
The hybrid checkout piece is where the prototype lied to me about what was hard.
The production app has a membership billing system. It is ASP Classic, it is old, and it works. A tournament event cart is a separate concept, a one-time purchase tied to a specific event, not a recurring membership cycle. When a tournament director registers for an event mid-billing-cycle, the checkout needs to produce one coherent total that includes any prorated membership adjustment and the event registration cost together.
The prototype was allowed to assume a clean billing boundary. The real implementation was not.
The arithmetic was not the central difficulty. The complication was applying the chosen proration policy through the legacy billing path, producing a line item the event cart could consume, and avoiding an inconsistent charge when the organization had already settled part of the period through another session.
That last constraint did not exist in the prototype. It showed up the moment I tried to wire hybrid checkout into an actual test org that had paid last month and was renewing next week.
I had not thought about it until that moment. The constraint never surfaced in the prototype.
where the gates actually live
Feature gates in the production app are not a single table. The surface I was wiring into is a feature-access resolver, a function that returns a bitmask of capabilities for a given org-tournament pair. Whether the org can create bracket rounds, publish a roster page, or use the event cart at all. All of it flows through this one surface.
Wiring tournament event pricing into the feature-access resolver took most of an afternoon. Not because the code was hard, but because the unlocks catalog had not been designed for bundle-ceiling semantics. The minimum-package gating pattern already used for a few other feature tiers assumes a fixed threshold. This feature is available at membership tier 3 and above. Bundle ceilings are not a threshold. They are a quantity that varies per event configuration.
The unlock logic had to learn a new pattern. Does this event have a ceiling defined, and if so, how many slots remain. That is a different kind of gate than any that existed before. Getting it into the catalog cleanly, without special-casing the feature-access resolver for this one pricing mode, added a day I had not planned for.
the work that was not in the prototype
Everything above was architecture. What follows was product.
For this product, an overdue account required a hard registration lock rather than a dismissible warning. That was a business-rule decision: a bookmark could bypass the context of an earlier notice, so the registration flow itself needed to enforce the account-state rule.
Pre-cap warnings. As an organization approached its bundle ceiling, the event cart needed to show that context before the organization committed. The shopping-cart model had no existing ceiling concept, so the warning required an informational representation that would not be mistaken for a transactional invoice line.
Downgrade behavior with preserved excess credit. If an org downgrades their membership tier after registering for an event covered by the higher tier, the registrations already paid do not get clawed back. The excess credit from the event is preserved as a line item balance against the org’s account. Writing that logic required touching the membership billing path in a way I had been deliberately avoiding.
None of this was in the prototype. The prototype had one org, one event, one happy path.
what the architecture doc is for
The implementation spans multiple separate codebases because development is split by function. Admin flows live in one place, public registration in another, billing in a third. A feature that touches all three lives in all three simultaneously, and each has its own branch cut point.
I wrote the architecture document not because I needed to think it through again, but because future me will not remember why bundle ceiling won over flat per-team. In three months, when a tournament director asks for a credits model, the document is the only thing that will stop me from re-litigating the entire choice from scratch.
The document is 47 lines. It covers the two-layer model, the proration contract, the gate semantics, and the three edge cases that shaped the design: mid-cycle checkout, overdue hard locks, and downgrade-with-credit. Forty-seven lines is enough to reconstruct the reasoning, and not a line more. It is not a spec. It is a decision record.
A pricing model becomes credible only after its assumptions have been checked against the real checkout path, the entitlement system, and the account states the product actually supports. Recording those decisions makes later changes easier to reason about.
Related
- The Migration Ticket Was Hiding a Landing Page: another example of a technical ticket whose real scope included product and distribution decisions
- The Page Was Performing a Ceremony, Not a Job: a related case where examining the existing contract before tuning implementation changed the work that mattered