Tickets Closed Isn’t the Same as Progress

When I took over the mobile team, we had just lost two directors in the space of a few months. What remained was a skeleton crew: one developer and one QA on Android, one developer and one QA on iOS. The workload ahead of us was substantial.

So at first, we hired whoever seemed capable of delivering, just to have extra pairs of hands. If someone had a track record of closing tickets, we brought them in and put them to work. It was the right call for the moment. But it created a problem I didn’t fully see until we were already living inside it.


Tickets closed isn’t the same as progress

Work was getting done. The backlog was moving. But bugs kept coming in, and nobody felt particularly responsible for them. The team had been hired to close tickets, so that’s what they measured themselves against. Code quality, maintainability, the long-term health of the codebase — those weren’t part of the deal they thought they’d signed up for.

I had to sit down with people individually and reframe the expectations. Every engineer on the team was personally responsible for the quality of code going into our repositories. That meant rigorous code reviews, for everyone’s code, including mine and the team leads’. Seniors were expected to model good code quality, but that didn’t exempt them from scrutiny. The newest hire had full freedom to flag a problem in a senior engineer’s pull request if it wasn’t up to standard.

Some people responded well to that. Others couldn’t make the shift, and we did have to let them go. That’s never an easy call, but a team where some people hold the standard and others don’t isn’t really a team. It’s just a set of individual contributors working in the same repository.


What we actually looked for

As we grew more deliberate about hiring, the technical bar stayed high but the questions got more specific. We looked for a firm grasp of the fundamentals: handling nullability, writing maintainable Kotlin, and spotting potential bugs in unfamiliar code. We’d give candidates some base code and ask them to extend it with additional parameters and optional logic, and watch how they approached it.

The question I found most revealing was simple: what’s your favorite feature of the language you work in? Most experienced developers have an answer, even if they’ve never been asked to articulate it. But I wanted the ones who could talk through it thoughtfully, the ones who lit up when they told me why they reached for it over other options. It was a window into how they thought about the language itself, and whether they were genuinely engaged with their craft.

Culture fit mattered just as much. Would this person make the team better? Would they engage in code review as a collaborative exercise rather than a defensive one? Were they someone who took ownership, or someone who was waiting to be told what to do?

HR was not always thrilled with how long our process took. But rushing a hire to fill a seat was exactly how we’d gotten into trouble before.


What the team looked like at the end

We grew from four people to over twenty. Each hire was deliberate, chosen for both technical skill and how they’d fit into and strengthen the team dynamic. Code reviews became a genuine part of the culture, with junior engineers pushing back on senior ones and knowledge transferring in both directions.

The real difference between the team at the beginning and the team at the end was ownership. People cared about the codebase because they felt like it was theirs. They took pride in the reviews, they flagged problems early, and they held each other accountable in a way that no process document could manufacture.

That shift doesn’t happen by accident. It happens because you hire people who are capable of it, hold everyone to the same standard, and make clear from the start that quality is everyone’s responsibility across the whole team and the whole development process.

For us, it showed up in the numbers. The new, dedicated team cut our production defects in half year over year and reduced our bug escape rate by 40%. That’s what a team with genuine ownership looks like in practice.

How I Got Permission to Burn It All Down

There’s a class of bug that will make you feel like you’re losing your mind.

It doesn’t happen every time. It doesn’t happen on command. It happens at 2pm on a Tuesday, or right when a client is watching a demo, and then it doesn’t happen again for three days. When you try to trace it, you’re watching five things happen simultaneously, any one of which could be the culprit. The answer changes depending on timing you can’t control.

That was the iOS app I inherited.


The problem

The app handled communications with a piece of external hardware that could send and receive messages at any time. Events could originate from either the hardware or the user, often in rapid succession. The original architecture handled each incoming message by spinning up a new thread. It was a reasonable approach to the problem, given the constraints at the time.

The problem was state. If the hardware sent a message assuming a certain state, and a user action had just changed that state on a different thread, you had a crash. Or worse, silent data corruption. And it was nearly impossible to reproduce reliably. Shift the timing by milliseconds and you’d get a different result. Unit tests couldn’t catch it. Code review couldn’t catch it. It just happened, unpredictably, in production.

When the previous team director left, we brought in outside contractors to assess the situation. One of them built me a diagram showing the call stack depth for a single event in the system. It looked like a subway map with no center. There was no reliable way to know, given any starting point, where you would end up.


The contrast

Meanwhile, our Android team had a similar product, built differently. Every incoming message dropped into a queue regardless of source. One thread pulled from that queue and processed messages in order, one at a time. No collisions. No ambiguity.

I took this contrast to my manager and walked him through tracing a single bug on each platform side by side. On iOS, you had to open multiple threads simultaneously, watch shared state being updated from different directions, and try to determine which update “won” and in what order. If the timing had been slightly different, the answer would have changed too. On Android, you followed the queue, read the log linearly, and wrote a unit test that reproduced it exactly.

He brought in the CTO. We showed him the same comparison.

The choice was clear. One system was consistently crashing. The other was stable. One had bugs you couldn’t reproduce. The other had bugs you could fix.


Making the case

The key was leading with the cost of the existing system rather than the appeal of a new one. The instability wasn’t just a technical problem. It was an ongoing drain on engineering time. Every hour spent trying to reproduce a threading bug was an hour not spent building features. Every contractor brought in to untangle the architecture was money spent with no clear end in sight.

A rewrite carried real risk. But continuing as-is meant accepting a steady, open-ended cost with no path to resolution.

We got four months approved to build an MVP. We adopted a state machine model with a message queue, where all interactions were traceable, all state transitions were explicit, the logic was unit-testable, and everything was debuggable. That became the foundation for an enterprise-level library the team could iterate on, test reliably, and actually reason about.


What I learned

Two things stayed with me from this experience.

The most persuasive argument for a hard decision is usually a comparison, not an assertion. I didn’t tell leadership the architecture was bad. I showed them what debugging looked like in each world and let the contrast speak for itself.

The other thing is that determinism is underrated. Engineers spend a lot of time chasing performance and new features, and not always enough time asking whether a system is actually predictable and testable. A system that is a little slower but completely predictable will outlast a faster, chaotic one, especially as the team and codebase grow.