Fixing a Ten-Year-Old Bug

I just published version 1.1 of Tomo Time, my simple Pomodoro timer app. It used to be called Tomatimo, ten years ago. Over the years, I got too busy to deal with the upkeep, Swift kept releasing new versions, the codebase fell further behind every year, and eventually even the UI looked dated because SwiftUI exists now and UIKit doesn’t get invited to the party anymore. The gap got wide enough that catching up felt like its own project.

A couple of weeks opened up on my calendar. I had an old repo and an AI agent ready to help me debug, so I went for it.

Sledgehammer first, precision later

Step one: upgrade the Swift version to something the latest Xcode would work with. Step two: fix the deprecation warnings, and there were a lot of them. Step three: find a SwiftUI bootcamp so I could get the UIKit out entirely.

At that point it felt like I was taking the house down to the beams and rafters just to modernize it. But it worked. The app was theoretically shippable again, assuming the old one had been.

The bug that survived seven years

There was one bug a user reported seven years ago: the app would forget where it was if it went into the background. I’d learned a lot since then, mostly about how aggressively iOS garbage-collects anything not currently on screen. The fix was simple in concept: serialize the timer state to JSON, write it to a file, and resync it to the current time when the app comes back to the foreground.

But my old data model was working against me. Old me didn’t mind double-tracking the same data — I was storing both how much time was left on a timer and when I expected it to end. That was fine when the overhead was minimal, but SwiftUI put more pressure on keeping the model clean, and adding serialization on top of that made the redundancy impossible to ignore. A running timer only cares about its end time, since remaining time is just a calculation from that. A paused or idle timer only knows its remaining duration, because it doesn’t have an end time yet. Tracking both meant every read had to guess which one was actually true.

The right shape for the data

In Kotlin, this would be a clear use case for a sealed class with data class members. Swift’s enum associated values are the closest native analog:

swift

case running(endTime: Date)
case paused(remaining: Double)

That change did most of the work, and Swift made the enum Codable automatically, so I got JSON and UserDefaults serialization for free. I didn’t need a private index to track which timer was “currently running” — I could just filter for the one that said it was running. Resyncing to the current time became a matter of finding the first timer with an expiration date still in the future.

It’s Interface Segregation for data models: don’t make a type carry state it doesn’t need. Ten years of drift, and the fix that finally stuck was making the model say what it meant.

When the Expert in the Room Isn’t Me

In late 2023, our bug escape rate hit 25%. One in four defects was making it to production without being caught in QA. Two major customers had upgraded at the same time, the combination surfaced more edge cases than we’d anticipated, and the numbers told a clear story.

It wasn’t that QA couldn’t do their job. The real problem was that they didn’t have the structure to do it well, and the structure was mine to fix.


What I knew, and what I didn’t

A year earlier, I had run a set of sessions with the developers around pull request standards, branching strategy, and handoff documentation. Those went well, because I already knew what good looked like. I have a software engineering background, and I could lead those conversations from a position of genuine experience.

But with QA, I don’t have the firsthand instincts that come from years of exploratory testing or building a regression suite from scratch. I needed to lead them toward best practices without knowing them beyond the principles myself. Walking into those sessions with a list of prescriptions would have been the wrong call.

I asked the QA team to teach me their job.


How the sessions actually went

I brought the whole team together and started asking questions. What does exploratory testing actually look like in practice? How do you know when you’ve covered enough edge cases? What needs to be on a bug report to make it actionable for a developer?

I did the typing while they worked it out together. Some voices naturally took up more space than others, and a few of the quieter analysts had the most useful things to say. I switched to asking them to raise their hands, to make sure every approach got heard. When something felt under-explored I’d push back with a Socratic nudge — “okay, but why do you think that’s true?” or “does everyone agree with that, or does someone see it differently?”

Here’s what was interesting: they all knew the theory. What they hadn’t done was compare notes on the practice. One analyst liked to write exhaustive test cases upfront. Another preferred to explore first and document later. A third had built custom tooling rather than reading through logs. Each approach had real strengths, and none of them had fully seen what the others were doing. The sessions gave them a structured opportunity to learn from each other’s workflows — to identify what worked well in someone else’s practice and figure out how to fold it into their own.

What came out the other side was a Work Process document they had written themselves. Because they’d built it together, they trusted it. They remembered the reasoning behind every line.


What changed

As we talked through their existing processes and what they wanted to see improved, a set of shared practices emerged from the conversations themselves. Explicit time budgeted for writing new test cases. A test review step so a second set of eyes could catch gaps. Clearer handoff expectations in both directions.

The bug escape rate dropped from 25% in December to 5% by the following quarter, well ahead of the 15% target we’d set. When we later hired a QA Manager, he was able to build directly on the Work Process document rather than establishing shared practices from scratch. He pushed the team further, introducing shift-left QA into the pull request process so defects got caught before code was already merged.


What I took away

When you know you’re not the expert in the room, you can create the conditions for the others to do their best thinking together. You check your ego, ask genuine questions, and trust that the people closest to the problem usually know more about solving it than you do.

The QA team didn’t need me to tell them how to test software. They needed someone to help them identify the answers they already had.