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.

Building it, Just Because

I’m a creative person; I like to make things. My “fidget toy” is crocheting little gifts for my engineering team during crunch week. Sometimes I paint, or I play music. And I write code for the same reason — because there’s something deeply satisfying about making a thing that wasn’t there before.

Modern culture likes to frame computer science as a discipline of pure logic. Vectors and matrices. Number crunching and data mapping. And yes, that’s true, but there’s an art to it. It sings. The developers I know have a creative outlet — music, woodworking, photography, and so on — and the ones who love building software tend to find that the craft itself scratches the same itch.


One problem, three ways

Here’s a simple example: given a string, return the most common character. If you’re in a functional programming mood:

val mostCommon = text.groupingBy { it }.eachCount().maxBy { it.value }.key

A nice concise one-liner. Until someone asks for edge cases in your unit tests.

Okay, we can prioritize some readability and maintainability here. The HashMap approach is still fine, but you’ll break it into multiple lines and leave some comments.

// Group characters and calculate frequencies in a single pass.
val charFrequencies = inputString.groupingBy { it }.eachCount()
// Find the map entry with the highest frequency count.
val highestFrequencyEntry = charFrequencies.maxByOrNull { it.value }
// Extract the winning character, default to ' ' if empty
val mostFrequentChar = highestFrequencyEntry?.key ?: ' '

It’s chattier, but the next person will understand easily where to start.

Or maybe performance is what you’re after. In that case you’d skip the HashMap entirely and iterate with an IntArray keyed by ASCII code:

fun maxAsciiChar(text: String): Char {
if (text.isEmpty()) return ' '
val counts = IntArray(256)
var maxCount = 0
var maxChar = text[0]
for (i in 0 until text.length) {
val char = text[i]
val code = char.code
if (code < 256) { // ASCII only
counts++
if (counts > maxCount) {
maxCount = counts
maxChar = char
}
}
}
return maxChar
}

Wordier and limited to ASCII, but it runs efficiently. The decision points are explicit, you know exactly where to put your breakpoints, and the unit test cases write themselves.

Three valid solutions to the same problem, each one a different expression of what you value. That's the art of engineering, shining through.


Build the thing that doesn't exist yet

The best way I know to keep that creative muscle alive is to build something just because you want it to exist.

Pick a problem that's yours. Make a weather widget and you'll learn about APIs. Build a to-read list that syncs across devices and you'll learn about user data. Or what I'm doing right now: build a music streaming app that generates endless lo-fi beats to work or study to.

That last one has been a genuinely fascinating learning experience, because the limitations I put on it are self-imposed. To make it sound right to me, the threads have to fire at the exact millisecond — even a tiny slip in timing pulls the listener right out of their concentration zone. The OS has opinions about what you're allowed to do in the background. Other developers have been here before you, and reading their solutions is half the education.

I have a background in digital music, so this project sits at the intersection of things I already love. But the specific topic almost doesn't matter. What matters is that you defined the requirements yourself, which means you're solving a problem you actually care about, which means you'll push through the frustrating parts instead of giving up when it gets hard.

And it will get hard. You will frustrate yourself. You will make mistakes that take two hours to find and thirty seconds to fix. That's the process: playing with the tools, learning from the mistakes, stretching a creative muscle you might not have known you had.

It can be hard to grind on side projects after a full day, and that's why building something you care about matters so much. The developer who stays interested enough in their side project to keep working on it, even when it's difficult, is the one who stays curious about what they're building.

Coding is a creative act — the act of building something new, from just an idea. Give yourself permission to play with it.

Six Weeks?!

Six weeks was the runway we had to rebuild a product that had taken ten years to develop, feature by feature, config by config. The custom hardware it ran on had reached end of life, and the business needed it running on Android hardware before the old platform went dark. There was no negotiating the deadline. There was only the work.

I tried to get ahead of it. I pushed to split the architecture handoff into two pieces: the networking library that everything else depended on first, and the UI and business logic second. That way part of the team could start two weeks early on the prerequisites while we waited for the rest of the spec. It was the right call, and it bought us some time.

Then the second dump arrived. Fifty pages of detailed technical specifications, with a refinement session scheduled for the following day. Not nearly enough time for anyone to build a mental model of what they were reading, let alone turn it into a coherent set of stories. We sat in front of a whiteboard with handfuls of sticky notes, trying to figure out what our story layout should even look like. We did our best. Maybe half the work got refined. With five weeks left and pressure mounting, we had to start building somewhere — refined or not. The team lead tried to finish refinement solo while the rest of the developers started chewing through the stories that were ready.

Go! Go! Go!


When the safety net got pulled

Before the first architecture dump even landed, a decision came down from above: cancel all unnecessary meetings. Concentrate. No distractions. That included 1-on-1s.

That was the decision that hurt us the most. My 1-on-1s aren’t status meetings. They’re the primary channel through which my team members tell me what they’re struggling with, where we can set goals and work toward them, and where we can pull things back before they become a crisis. Cancelling them at the very start of the most stressful project of the year didn’t help the team concentrate. It left them without a reliable way to escalate, and left me without the insight I needed to help.

All I had was the morning scrum. And by the time something surfaced there, the runway to fix it was already short.

So when problems started happening (and the incomplete refinement made sure they did) there was very little I could do about them in time. No working agreement meant no plan for pulling things back on track. Scope doubled. Last minute requirements jumped the line. Must-fix bugs as far as the eye could see.

I stayed online with the team through weekends to keep them unblocked. I was up until 2am helping debug where I could. I negotiated with product to cut even one requirement so the rest could ship on time. We were working hard instead of working smart, and we all knew it.

The team made the deadline. Ninety-five percent complete, against all odds. They went straight into bug-fixing mode the following week, chasing must-fixes and as many nice-to-haves as they could reach. And my job became documenting everything that had gone wrong, so that it would never happen again.


What I wish we’d protected

The lesson wasn’t about balancing deadlines or managing technical complexity. It was simpler than that.

When a crisis hits, the instinct is to throw out everything that feels like overhead and just run. Cancel the meetings. Skip the process. Heads down, ship it. But the things that feel like overhead in a crisis are usually the things that were keeping the team functional. The 1-on-1s. The refinement sessions. The working agreements. The feedback loops. Strip those out and you don’t get a leaner, faster team. You get a team running in the dark, working harder instead of smarter, with no early warning system and no way to course-correct.

Honestly, I wish I’d pushed back harder on the 1-on-1 decision. I understood the pressure, and I tried to work within the constraints I was given. But those conversations were load-bearing, and losing them cost us more than the time they would have taken.

Protect your Process.

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.

Building Signposts for C# Engineers Racing to Kotlin

A while back, leadership came to me with an unusual staffing proposal. They had a pool of experienced C# developers they wanted to place, and I had open Kotlin positions that required deeper experience than flashy UI and a server API — the kind of skillset that’s genuinely hard to find on the open market. Could we bring these developers up to speed on Kotlin and fold them into the mobile team?

The developers already knew the company. In this organization, most teams didn’t have any real trouble pulling in a developer from outside their vertical. They would need to learn new tooling, maybe a few new libraries, but not a new programming language and hardware stack. But Kotlin is not C#. We needed to bridge that gap efficiently to solve our staffing problem.


Lost in Too Much Familiar Territory

The easy answer? Tell these developers to go read a book, watch YouTube tutorials, or enroll in a Kotlin bootcamp. But the problem becomes obvious pretty quickly.

Most Kotlin learning material is written for a general audience. It starts at the beginning. Data types. Variable declarations. How to return a value. All perfectly reasonable if your reader has never programmed before. But a developer with years of C# experience doesn’t need that level of pedantry. They start skimming, and skipping, and somewhere around page 127, buried in a chapter they half-read, is the explanation of scope functions that would have saved them three hours of confusion next Tuesday.

And self-study has its own trap. Sure, they’ll eventually need to stop writing endless else if chains — but who’s going to tell them to look up when? Who will they trust to explain the difference between let, apply, also, and run? It’s not always obvious where to start, or even that there’s something worth looking for.

We needed something that would show them all the tripping hazards and shortcuts, not teach them how to walk all over again.


Planting the Signposts

Once we identified the current skillset of our audience, we could target the training to their experience level. The skill gap for an experienced C# developer learning Kotlin isn’t the basics. It’s the subtle differences and valuable tools that Kotlin uses to make a developer’s life easier.

I started with a one-page summary of data types — not the ints and Strings they knew by heart, but the core concept of mutability (consider a var List vs a val MutableList) and nullability (why the perfectly obvious !! can cause your app to crash). Once that was established, we could move on to the things that have no real C# equivalent: scope functions, when expressions, data classes, sealed classes. Each one got its own focused one-pager rather than a footnote buried in a chapter about something else.

I wrote a whole series of focused one-pagers, each covering one concept, each with exercises. I wanted our incoming developers, who already understood the basics of programming, to work through the whole thing in about a week. They would come out with a mental map of how Kotlin thinks, and enough confidence to navigate the unfamiliar territory on their own.


Learning to Run

Our first volunteer had spent years working in a low-level language and was genuinely excited to be working in something higher-level again. He went through the entire guide in a single day. Then he started building small apps to prove to himself — and to me — that he’d understood the principles well enough to keep going independently. The guide showed him the terrain. He did the running himself.

It’s easy to assume that training needs to be thorough — start from the beginning and walk forward methodically so nothing gets missed. But experienced developers don’t need a comprehensive guide to get themselves started. They need a few pages of guidance on what’s different, signposts for where to take the next steps, and trust that they can take it from there.

There are plenty of Kotlin textbooks out there if you really want one. But you might save yourself some time by minding the gap.

KMP is What Good Multiplatform Looks Like

When our mobile team started looking seriously at Kotlin Multiplatform, we had a people problem on our hands, not a technology one.

We had two native teams, iOS and Android, building the same business logic twice. The same data calls, the same validation rules, the same utilities, implemented separately, tested separately, and occasionally drifting apart in ways that were subtle enough to miss until a client noticed. Every time the requirements changed, we made the same update in two languages across two codebases just to keep parity.

KMP gave us a way out of that.


What we actually shared

Our first focus was two key areas where we could add consistency and predictability. The first was data processing: building shared logic to generate summary reports from repetitive data transformations. The second was network handling: a shared front-end on our APIs that kept retry logic and error handling consistent, regardless of which platform was making the call.

The trick is to keep the scope small. KMP works best when you’re sharing logic that is genuinely platform-agnostic. Threading strategies in particular deserve caution. Anyone who’s been there knows that getting clever with concurrency across platform boundaries is exactly the kind of thing that produces hard-to-diagnose bugs.

Our migration was partial and deliberate. We didn’t try to share everything at once. We identified the business logic that was most duplicated and most stable, extracted it into a shared KMP module, and let both teams integrate it incrementally.

It made the right problems easier to solve, even if it didn’t solve all of them.


The skepticism

Not everyone on the team was immediately on board. For the iOS developers in particular, the ask was a real one: learn Kotlin, a language they hadn’t been hired to write, in order to contribute to a shared codebase that was built around Android’s primary language. That’s a fair concern and we took it seriously.

In practice it went better than expected. Kotlin and Swift are remarkably similar in design. Both are modern, expressive, null-safe languages that share a lot of the same patterns and idioms. I’ve taught multiple Swift developers to work in Kotlin and not one had a real technical issue picking it up. There are even a few places where Kotlin pulled ahead of Swift — the flexibility of when compared to switch being a favorite example — but that’s a conversation for another post.

The resulting shared module reduced the surface area for cross-platform inconsistencies, and the unit test coverage on shared logic actually improved because we only had to write it once and we knew both platforms were running exactly the same code.

Unlike other cross-platform tools, KMP is built from the ground up for mobile developers. Once the iOS team started working in it, that became obvious pretty quickly.


Why it was a good decision for the business too

Kotlin Multiplatform lets you write shared Kotlin code that compiles to native binaries on each target platform. On iOS it compiles to a native binary. On Android it compiles to JVM bytecode. There is no intermediate interpreter, no bridge layer, no runtime sitting between your shared code and the platform it’s running on. KMP doesn’t ask you to abandon your existing native codebase. You introduce it where it makes sense, layer by layer, and the rest of your native code works just the way it used to.

Upper management was initially worried that KMP didn’t have enough momentum to be worth the investment. The answer has gotten clearer since then. KMP usage more than doubled among multiplatform developers in a single year, rising from 7% in 2024 to 18% in 2025. At Google I/O 2024, Google announced official support for using KMP to share business logic between Android and iOS. JetBrains has been a reliable steward of developer tooling for over twenty years, and with Google now formally behind the technology, the risk profile looks very different than it did even two years ago.

The ecosystem is still maturing, and the library coverage isn’t as broad as some alternatives yet. But for a native mobile team that wants to reduce duplication without giving up the performance, platform integration, and developer experience they’ve built expertise around, KMP is the most natural fit on the market right now.

From Senior Engineer to Director: A Story About Trust

Being a director means accepting that you can’t review every PR personally or weigh in on every architectural decision. Trying to do both produces engineers who stop thinking for themselves because they know you’ll think for them. The job is to build a team you can trust, and then trust them.

That’s harder than it sounds. As an engineer, your judgment is grounded in what you know. You can evaluate a solution because you understand the problem. But a director is regularly asked to evaluate solutions to problems that sit outside their immediate expertise. The question stops being “is this technically correct?” and starts being “do I trust the person bringing this to me, and have they earned that trust?”


How trust gets built

Trust on a technical team isn’t given, it’s built over time and effort. I track it through the things that are actually visible: a developer’s commit history in the codebase, the quality of the code reviews they give and receive, the features they’ve shipped that have passed QA without drama.

Over time, a picture forms. Some engineers consistently bring clean, well-considered work. Others bring good instincts but need more guidance. A few surface tech debt that genuinely improves quality of life for the whole team, which tells you they’re thinking beyond their own ticket.

At the same time, I’m working to earn their trust as their director. They need to know that when they bring me a real problem, I will either help solve it or find someone who can. That consistency is what makes them comfortable surfacing issues early rather than sitting on them until they become crises.


Trusting the track record

My tech lead came to me with a proposal to refactor one of our smaller apps around a state machine pattern, using Jetpack Compose. The pitch was that predictable inputs and outputs for any given screen would reduce our error rates significantly.

Jetpack Compose was still relatively new, and our team didn’t yet have deep experience making it work well in production. I couldn’t evaluate the technical specifics from first principles. What I could evaluate was his track record: consistently well-regarded by his peers, thoughtful in his reviews, right on his previous proposals. That was enough to work with.

Rather than defaulting to skepticism, I worked with him to develop the proposal into a full development plan to bring to the product team. The refactor took about two months. Since then, that app has needed only minimal updates to add new hardware support.

This project wouldn’t have gotten off the ground without trust working in both directions. He trusted me to champion something he believed in, and I trusted his judgment on how to accomplish it.


What building trust actually looks like

Trust flows naturally from the structures you put in place: code reviews that give engineers real visibility into each other’s work, training that raises the floor across the team, clear expectations about what good performance looks like. The more deliberately you build those structures, the more confidently you can trust the judgment that comes out of them.

The goal is a team where everyone is bringing their best thinking to the table, and where the director’s job is to synthesize that thinking, clear the path, and back the right ideas. When it’s working, the team can do things that no single engineer could do alone.

The Feedback Loop is a Feature

I’ve noticed that the best software teams have one thing in common: they get better over time. Features ship faster. Bugs get caught earlier. Cross-team dependencies cause less chaos than they used to. It usually comes back to the same thing. They have feedback loops, and they’ve built them deliberately.

A feedback loop is a simple system for surfacing information early enough to act on it. The earlier you catch a problem, the cheaper it is to fix. That principle scales from a single line of code all the way up to a cross-team architecture review — and the teams that apply it at every level are the ones that compound.


Small loops, big returns

The easiest feedback loops to build are the small ones inside your own team. They don’t require negotiation with other teams or sign-off from leadership. They just require someone paying attention and a process for acting on what they hear. Our team holds regular retrospectives to solicit feedback on anything the team wants to improve — tooling, process, workflow, whatever is slowing people down or making their day harder.

One of the more impactful suggestions came out of one of those sessions. The idea was simple: one analyst writes up the test cases, a second reviews them to check for gaps, and then either analyst can run the tests because both understand them fully. Analysts with different testing styles could backstop each other, which meant the test suite got more complete over time rather than reflecting the blind spots of any one person. We had a structural improvement that raised the floor on our release quality.

But feedback loops need to close quickly. Feedback that goes nowhere teaches the team that feedback doesn’t matter. Every suggestion that gets implemented, or thoughtfully declined with a reason, reinforces that the channel is real.


Structure the conversation early

The bigger the dependency, the more expensive the feedback loop becomes if you build it too late. When we were integrating against an API being built by another team, we had a chance to apply this lesson on the second version of that integration.

Before anyone wrote a line of code, my lead developer asked me to put together a step-by-step integration guide that laid out exactly how our UI expected to use the interface and what error cases we anticipated encountering. We presented that framework directly to the server architects and asked them to redline it, push back on our assumptions, and flag anything we’d gotten wrong.

The result was that a significant portion of the hard integration work got done on paper before it got done in code. Both teams understood the contract. The architects could design around our actual needs rather than their assumptions about them. And when surprises did come up, they came up in a document rather than in a sprint.

That’s what a feedback loop looks like at the cross-team scale. You’re giving the team a structured opportunity to get their needs addressed. This will surface the issues earlier, when they’re still cheap to resolve.


What makes any of this possible

Whether it’s a QA process review or a pre-integration framework document, the underlying principle is the same. Find the point where information needs to travel between two parties, and build a structure that makes that travel faster and more reliable.

But none of it works without psychological safety. The QA suggestion came from analysts who felt comfortable saying “our process has gaps.” The integration framework came from developers who felt comfortable saying “we need more structure around this before we start.” Both of those conversations require people to be willing to surface a problem before they have the solution.

That willingness only happens on a team that trusts its leadership to hear problems as information rather than complaints, and to work through them collaboratively rather than defensively. When that trust exists, your team becomes your best early warning system. They will bring you the questions, the concerns, and the half-formed ideas before they become crises — and they will help you solution your way through them.

Rubber Ducks and Hard Deadlines

Crunch time.

Every software team hits it eventually. A surprise requirement, a deadline that can’t move, a week where everyone is going to have to dig in. The question isn’t whether it happens. It’s whether your team comes out the other side intact.


When the surprise lands

In July 2024, a significant unplanned requirement came in with a firm external deadline. I was on vacation when it happened, which meant the news reached my team before it reached me. By the time I was in the loop, the decision had already been made to bring the remote team into the office to encourage in-person collaboration during the push.

There was grumbling. Of course there was. Nobody loves being asked to travel on short notice, and nobody loves having a surprise dropped on them mid-summer. That’s a completely human response to a genuinely inconvenient situation.

My job wasn’t to manage the grumbling away — it was to really listen. My job was to make sure my team knew I saw them as people first. That they weren’t just resources being deployed to solve a problem. If I could establish that clearly enough, the rest would follow.


Getting the ducks in a row

I hand-wrote a thank-you note for each team member, acknowledging their specific contributions and what I valued about working with them. I also hand-made each person their own little rubber duckie.

If you’re not familiar with rubber duck debugging, it’s a real technique — explaining your code out loud to an inanimate object helps you spot your own errors. I found a low-sew crochet pattern online, used Caron yarn, and attached a keychain loop to the top of each one so people could hang them nearby. The goal wasn’t to bribe anyone into good spirits. It was to signal, as concretely as I could, that I was thinking about each of them individually.

One team member who had recently transferred from another department seemed genuinely surprised to be singled out for appreciation. On his previous teams, crunch was just expected. Being acknowledged for showing up felt unfamiliar to him, and watching him recalibrate a little was one of the more memorable moments of that week.

My team members still have their duck friends at their desk today.


A promise on both sides

The other thing that made this crunch survivable was being firm about scope. My scrum master and I built a report that tracked the work story by story, showing where we expected to hit our targets, where we were at risk of falling behind, and what we planned to do about it. The engineers could see at a glance what the expectations were and what levers they could pull when things got tight.

That visibility mattered. When scope is defined and the finish line is real, people can push toward something. When scope keeps expanding, the finish line keeps moving, and that’s what burns people out. By holding firm on scope with the product team, we could make honest commitments to the engineers. Each side was making a promise. Each side could see what they were getting out of the deal.

We did have one last-minute addition land in the final weekend before delivery. We worked through it. But because the team trusted that we were protecting them from unnecessary scope creep wherever we could, and because they understood why this one couldn’t wait, they got through it without it feeling like a betrayal.

We hit the deadline.


What actually makes crunch survivable

It isn’t perks or free dinner. Those things aren’t bad, but they’re not the point. The point is trust that was built before the crunch ever started.

If your team knows from consistent experience that you will go to bat for them, that you notice their work, that you see them as people and not headcount — then when you need them to dig in, they will. Not because they have to. Because they want to see it through together.

One team member put it simply after that week: “Your leadership kept it bearable.” Another wrote: “Just want to say I appreciate the way you handled the situation.”

That’s what I was going for. Not heroics. Just people who felt seen, working together toward something they understood and believed in.

The ducks helped too. In more ways than one.