The Feature I Built Twice Because I Was Afraid to Ask One Question
"I had built exactly what the ticket described. The ticket had described the wrong thing."
The ticket had been sitting in the sprint for three days before I picked it up. It was short: "Add a seven-day grace period to subscription renewals. Customers whose subscriptions expire should continue to have access for seven days while we retry payment." Clear, specific, actionable. I started building.
A week later, I opened a pull request. The implementation was clean. The tests covered the main scenarios. I had added the grace period logic to the subscription state machine, written the payment retry job, and updated the billing dashboard to show which subscriptions were in their grace period. I was pleased with how it had come together.
The product owner reviewed the PR during our sprint review. She read through the implementation carefully. Then she asked a question.
"What happens to a subscription that fails payment twice in a row, across two separate billing cycles?"
I described what would happen. The subscription would expire. It would enter the grace period. Payment would be retried during the grace period. If the retry failed, the subscription would be cancelled after seven days.
She nodded slowly. "Right. And what I needed was for the grace period to only apply to the first failure. If a subscription has already been through a grace period in the previous billing cycle, we don't offer another grace period, we cancel immediately on expiry."
I had built the wrong feature.
Why I Got It Wrong
I had not asked about the business context behind the requirement. The ticket described a mechanism, "add a grace period, retry payment", but not the purpose of that mechanism or the constraints that should govern its behaviour. The purpose was to recover genuinely recoverable payment failures: temporary card declines, banks blocking unfamiliar transactions that the customer would unblock if notified, short-term cash flow issues that the customer would resolve in a few days. The constraint was that the grace period wasn't meant to be exploitable or indefinite, a customer who had already failed payment in the previous cycle and not resolved it was a different case that the business wanted to handle differently.
None of that context was in the ticket. It was in the product owner's head, and it would have been in my head too if I had spent thirty minutes talking to her before starting the implementation rather than after finishing it.
The assumption I had made, that the grace period was a universal feature that applied the same way every time a subscription expired, was reasonable given what the ticket said. The ticket's description was accurate for the common case. It simply didn't mention the exception, and the exception was important enough that the feature without it would have required immediate further work.
The Hidden Cost of Building the Wrong Thing
Rebuilding the feature took about two days, less time than building it the first time, because the basic structure was in place and the change was targeted. From a calendar perspective, the cost of the mistake was two days of engineering time. That calculation, though, misses most of the actual cost.
The rework came out of the sprint, which meant something else in the sprint slipped. The feature wasn't shipped until two weeks after it was first completed. The subscription billing logic, which I had already integrated into the state machine, had to be partially restructured to handle the new distinction between first-failure grace periods and subsequent-failure cancellations, which introduced new tests and a small amount of architectural complexity that a correct first implementation wouldn't have had. The pull request had to be abandoned and restarted, which meant review context and comments were lost.
None of this is catastrophic. But it is all cost that occurred because of a single missing conversation, and it is cost that compounds when it happens repeatedly across a team over time. The practice of building confidently from incomplete requirements, filling in the gaps with assumptions rather than clarifying them, is one of the most persistent sources of avoidable rework in software development, and it's hard to see clearly because the individual instances look small.
What a Domain Expert Actually Knows That the Ticket Doesn't
When I finally sat down with the product owner to understand what she needed, the conversation lasted forty-five minutes. In that time, I learned several things about the subscription billing domain that the ticket had not mentioned and that I would have needed to know before shipping any version of this feature:
Grace periods are regulated in some markets. In certain jurisdictions, consumer protection laws specify minimum grace period requirements for subscription services that are billing annually, longer than seven days, in some cases. The implementation needed to be configurable by market rather than having a hardcoded seven-day value.
The retry logic needed to distinguish between recoverable and unrecoverable payment failures. A card decline due to insufficient funds is potentially recoverable in a few days. A card decline due to the card having been cancelled or reported stolen is not recoverable through retries. Retrying an unrecoverable failure wastes resources and can flag the merchant account with the payment processor for excessive declines. The retry schedule should consider the failure reason code from the payment processor, not just whether the payment succeeded or failed.
The grace period state needed to be visible to customer support. Support agents needed to be able to see whether a customer was in their grace period, how many days remained, and whether any retry attempts had occurred, in order to have informed conversations with customers who called about their subscription status. This was a display requirement that wasn't in the ticket but was necessary for the feature to be operationally useful.
The product owner had been thinking about all of this for weeks. It was obvious to her, so obvious that she hadn't thought to write most of it down, because the context felt like background. It wasn't background to me. It was the critical specification I needed to build the right thing.
What Changed After This
The pattern I developed after this experience: for any feature that touches a domain I haven't worked in before, or where the ticket describes a mechanism without explaining the business purpose, I schedule a thirty-minute conversation before I start the implementation rather than after.
The questions I ask in that conversation are usually variations of three:
What is this feature trying to achieve, and for whom? Not "what does it do", what is the underlying goal, what problem is it solving, who is affected by it. This question surfaces the purpose behind the mechanism, which then lets me ask intelligent follow-up questions about edge cases.
What's the worst thing that could happen if it behaves incorrectly? For a subscription grace period, the answer is that customers get continued access to a service they haven't paid for, which is a revenue leakage problem, or that genuine customers are incorrectly cut off, which is a customer satisfaction problem. Knowing the failure modes tells me which parts of the implementation need to be bulletproof and which have more tolerance for imprecision.
What doesn't need to be in the first version? This question is less obvious but often the most useful. Every domain expert has a list of edge cases they know about and aren't expecting you to handle immediately. Getting that list means you can acknowledge it explicitly, "I'm not handling the annual subscription case in this version", rather than discovering it in review after you've built something that doesn't cover it.
On the Fear of Asking
The instinct to start building rather than asking is not irrational. Asking questions takes time. It can feel like it signals that you don't understand the requirement, which can feel like it signals incompetence. It interrupts people. There is a real culture in some engineering teams that treats requirements questions as a failure mode rather than a normal part of the process.
That culture is worth examining. A developer who asks clarifying questions before starting is not slower than one who starts immediately, they are faster, because the time spent clarifying is almost always less than the time spent rebuilding. A feature that matches what the business needed is worth more than a feature built quickly and built wrong.
I have rebuilt my own work a surprising number of times. The instances that I look back on without frustration are the ones where I discovered the misunderstanding early, through a conversation, before any code existed. The instances I wish had gone differently are the ones where I felt confident I understood the requirement, coded it, and found out in review, or, worse, after shipping, that the confidence had been misplaced.
The thirty minutes before the sprint is almost always worth the week it saves after it.
Tags