I was sixteen when I first read The Pragmatic Programmer. Back then, my programming career was in its very early stages—crude websites, silly games, and even sillier quizzes. The code I wrote was a shoddy, incomprehensible mess (for a long while, I preferred creating a single massive file, instead of modularizing the code in some way). I had only begun to learn that the job of a software developer goes beyond just making it work, that you’re writing code not for machines but humans, and a quote from The Pragmatic Programmer had a catalytic effect on me in that regard:
There’s no point in making software if you don’t want to make it well.
When I was finished with the book, I knew I had to fix my lazy approach towards programming. Code that worked was a trivial goal; the real holy grail was clean code. In the ensuing projects, I put every ounce of effort in doing what The Pragmatic Programmer expected—or what I thought it expected—me to do: no line of code should be duplicated; every flag, however trivial, should be customizable via a configuration file; the abstractions should be solid and future-proof. Much to my client’s reservations, I began to spend time writing tests rather than just build features which he would have preferred.
Was it worth it?
Fighting technical debt is a never-ending battle. The team might have made a silly, short-sighted decision in the past, seemingly reasonable for their constraints, and today, it is bearing its consequences. The situation presents a dilemma—to carry on the debt, or set aside resources to undo the mistake. But, let’s not get into that. What we are interested is in what happens afterward.
The team learns its lessons and commits to not make more silly, short-sighted decisions. That means more meetings in deciding the correct approach, insistence on being future proof, more processes, and checks. Of course, that also means average developer productivity has gone down, but that seems an acceptable expense.
The question to be asked again: Is it worth it?
In the ideal world, where what needs to be done is crystal clear, and the dynamic between the development team and sales, marketing, and product teams is perfectly harmonious, the answer would be yes. But, the real-world situations are starkly opposite. The company’s future vision, even if crystal clear, doesn’t prevent things from being chaotic at the granular level. And that’s when trying to avoid technical debt at every step seems more like a pointless obsession.
In fact, that’s often been my source of frustration working in the corporate world—too much planning that gets thrown out of the window when things don’t work as expected. To cite my favorite example:
Not long back, I had to make a registration page with a little to no bells and whistles—a form with basic styling. Ideally what should have been a two days job became a needlessly stretched, monotonous project lasting two weeks. I couldn’t just build the form in a single HTML file, I had to do it “the proper way.” I had to set up SCSS, webpack, linting, and a makefile. I had to import modules and utilities from existing projects. I had to spec it before I started working, which also meant doing multiple meeting. “The proper way” that was meant to avoid incurring the cost later on.
Fast-forward six months, that page lies unused. The marketing team (probably) figured out that the same form built in Wordpress can save them the frustration of asking developers to do trivial changes, who might not appreciate the “unplanned work.”
You can’t blame the marketing team for opting a convenient option for themselves, you can’t blame the dev team either for not anticipating the decision. But, why doing things the right way needs to be such a fixation? Isn’t it better to solve the immediate problem rather than planning in exquisite detail for the future? If there’s a cost associated with technical debt, perhaps there is a cost to its avoidance as well.
When I was dogmatically following The Pragmatic Programmer, I made the same mistake: building abstractions that made little sense in the long run, making components configurable that would never change in the first place. I wanted to take the long view, but inadvertently, I was over-engineering.
Accepting Technical Debt
The code doesn’t evolve in isolation, it evolves with the business goals of the company, which are unpredictable on a long timescale. Anticipate in mild form and it can turn out to be helpful, but do anything beyond that, you are solving invented problems.
The software world is rife with examples of that. Why do we have websites designed as a single-page application when plain HTML would be much better? Likely, the thought process involved having a grand vision of what the app will need in the future and so, it was only obvious to involve React and SPAs from the get-go.
The truth is you are better off solving the problems that are the most immediate. Avoiding technical debt entirely is nothing but a delusion. There will be a decision at the end—no matter how well thought out it was—that you’ll end up regretting. The goal, rather, should be to make it easier to accommodate for those mistakes. Refactoring, for instance, would less scary if there is a) testing in place b) changes can be deployed in a piecemeal way c) and they can be easily reversed.
Because we have pretty comprehensive test coverage, we can afford to be aggressive with refactoring. There are always a few major refactor efforts going on in Chrome.
If a refactor breaks something that wasn’t exposed by failing tests, our outlook it that it wasn’t the fault of the engineer who did the refactor, but the one whose feature had insufficient test coverage.
– Aaron Boodman, Chrome Dev Team
Technical debt is a part and parcel of evolving code. Yes, in some places, it’s more pronounced: a codebase that has become an impenetrable mess after years of disregard towards refactoring. However, that is more a result of apathy than a single decision, an unlikely situation if your team cares about doing better. Paul Graham couldn’t have said it better:
If you're a decent programmer working on a startup, take on as much technical debt as you can stand. (If you're a decent programmer, you won't be able to stand too much.)— Paul Graham (@paulg) January 7, 2019
Care about writing good code, but not too much.