Experience is valuable in software because, over time, you see lots of different technologies, get a chance to try out different approaches to problems, and build up all the little ancillary skills that make you more efficient and effective. One particular kind of experience that I think is super valuable, and too rare, is the experience of working with the same codebase for a long, long time.
Early in my career, I worked on the same codebase for over 10 years. Naturally, I learned a lot in that time — I was self-taught and pretty new to software when we launched that project. Only after leaving that project and working at a much more rapid (and typical) cadence have I been able to appreciate how special that long-term tenure was as a learning opportunity. (I’ve probably contribut…
Experience is valuable in software because, over time, you see lots of different technologies, get a chance to try out different approaches to problems, and build up all the little ancillary skills that make you more efficient and effective. One particular kind of experience that I think is super valuable, and too rare, is the experience of working with the same codebase for a long, long time.
Early in my career, I worked on the same codebase for over 10 years. Naturally, I learned a lot in that time — I was self-taught and pretty new to software when we launched that project. Only after leaving that project and working at a much more rapid (and typical) cadence have I been able to appreciate how special that long-term tenure was as a learning opportunity. (I’ve probably contributed to 15-20 projects over the course of the next ~8 years.)
A long journey filled with painful lessons
So what are the lessons that this long tenure gave me, that I might not have learned as well through project hopping? It started with a very familiar feeling: somebody made some stupid decisions and it’s making our lives hell as we try to maintain or extend the code. But because that “somebody” was always me X years in the past, it ended up somewhere else entirely.
At first I was tempted to make excuses. “That’s just how it works because I designed it to be so amazing in this other way! Totally worth it!” When forced to endure the pain long enough, though, I’d have to accept that it really wasn’t working out and cut my losses.
Lesson learned, right? That one specific choice was so bad, I’ll never do it that way again. I’d slow feature development for a week while I ripped the cursed thing out and replaced it with something much better, informed by all the wisdom I’d accumulated since the original decision. Maybe I’d even get a chance to tell some n00b on Stack Overflow how much better the new thing is than the old thing, so they wouldn’t fall into the same trap. True enlightenment!
But then, slowly, the new solution would start to show some rough edges as well. I’d designed it for maximum flexibility in a way the original code lacked, but now reality was trying to stretch and bend it along different, unanticipated axes! How could this happen? Were the customers or product managers wrong? Was the programming language or database to blame? Or was I just, maybe, not good at this?
This process happened over and over again, with different specifics each time. There were definitely lots of concrete, hard-won technical learnings that inform my decisions to this day. But in hindsight, things often just played out in ways that I couldn’t have anticipated. No matter how much research and planning we did before building and launching, we always learned something new and surprising when the feature got into the hands of real users. And we continued to be surprised as time went on, not just with new features but with how existing features were received by new cohorts of users.
I gradually realized that the challenge is not to build the system to succeed in a specific, predictable future, but in some unknown future I couldn’t see. I could guess at the contours of that future, but given the level of uncertainty, I’d be wise to learn to reduce the impact of being wrong.
How did this change how I approach building software?
I try not to judge design decisions on whether they end up being prescient, but on how easily they can be changed when we learn exactly why they weren’t. Instead of elaborate, future-proofed code, I try to encourage small, simple bits of code that are easy to move around, change, build on, or even delete.
I also saw that complexity is much easier to add than it is to remove. And the costs of that complexity are paid again and again over time, as you have to maintain and modify the complex thing. So now I try to “engineer the requirements” to avoid any complexity that’s not absolutely necessary.
When the time does come to delete code, I’m happy to :zap: it, regardless of how clever it was, who wrote it, or how long we worked on it. If it helped us learn why users hated the feature, we got a ton of value out of it, even if it’s left the codebase.
Another thing that’s useful is to appreciate the haziness of the line between Type 1 and Type 2 decisions. When faced with what feels like an irreversible decision, carefully planning the scope, implementation, and rollout of a change can make it more easily reversible than initially thought.
Finally, over time I’ve noticed that some “bad code” gets in the team’s way all the time, introducing bugs and slowing down feature work, while other examples may be just as ugly but quietly get the job done with little or no attention for years. So I try not to pay attention to code quality as an inherent, objective thing that speaks to our worth as a software development team.
Instead, I like to gather data about the way a particular piece of code or design impacts our ability to meet our larger goals. (How many bugs has this file produced in the last 6 months? How many times has it been edited? How often have tasks touching this subsystem been delivered slower than expected?)
How does this compare to codebase hopping?
There are definitely downsides to spending so much time working on the same piece of software. There are fewer opportunities to keep up with trends and try out the new hotness — be it a new paradigm, language, framework, or library. Code reading skills build more slowly if you’re spending all your time in a relatively homogeneous and familiar codebase. As systems mature, there are fewer of those “this is why I love programming!” problems and more of those “what’s best for the overall stability of the system?” problems, making it difficult at times to maintain your passion for the work. But somebody’s got to put in the work to learn those lessons!
In my current role, I get to learn about the hardest problems faced by our talented teams across many different projects. Sometimes my years in the trenches allow me to offer valuable insight and help some of my colleagues “skip the line” by not having to learn the hard way. Just as often, I get to learn about creative, clever, and pragmatic solutions to problems I’ve not yet encountered myself, and I’m grateful for every day I get to help keep that flywheel spinning.