A Field Study on Technical Debt
blog.sei.cmu.edu
blog.sei.cmu.edu
The reason I bring this up is I am surprised that architectural choice is overwhelmingly the top technical debt concern. That doesn't seem right. I would expect it to be poor code due to time constraints.
First question: "Backups?" followed by "When was the last time you did a restore?"
Second question: "What's your revision control?" For all of my slagging on git and the people who use it, I am thrilled if I hear "CVS" or "Subversion" because it means they're using version control. "git" or "mercurial" tells me I have an team that has some level of clue.
Third question: "Build system?" Good luck. Never yet seen it.
I think, at least in sfbay, it's safe to assume git. And probably some CI tool. This is true across my personal sample of perhaps 15 companies that I know well enough.
The Bay Area is better than most places, but the moment you aren't dealing with a purely software shop, the probability they don't have this stuff goes through the roof.
The PCB design tool Altium only has provision for using Subversion (which is fine, thanks), but the number of people who actually USE it is miniscule.
KDE almost lost hundreds of their git repositories in 2013 because they were not doing backups ( https://news.ycombinator.com/item?id=5432760 ).
1. No backups. Emergency plan = several older copies of the product's source spread among hard drives of various former employees' systems.
2. No source/revision control. Current version of the product's source code was literally whatever was on the lead developer's hard drive at the moment.
3. No deployment process. When a release had to happen, above mentioned developer would build an EXE out of whatever he had (if it built) and directly deploy it to customers.
4. No dependency management. What versions of vendor-provided libraries do we use? Whatever is on the guy's hard drive. Oh, and they're all binaries because we either lost or can't build the source anymore.
5. No bug tracker. The list of stuff that needed to get worked on was whatever the CEO complained about last. There was no concept of a backlog of existing bugs or technical debt.
6. No documentation. But are you surprised at this point?
7. Setting up an environment to build the code was a manual process that relied on searching through past E-mails for lost tribal knowledge, which basically amounted to: Try different include and library paths until there are no errors. Once built I was greeted with 2500+ compiler warnings... (out of 200 or so C++ files).
Oh, and this software ran in airplanes.
What were you able to do to improve the situation?
Which issue did you tackle first (documentation of processes backwards or source control forwards)?
Tackled source control and issue tracking first since they were low hanging fruit. Next came basic (manual) backups. My background is not in putting together general office-wide IT solutions, so I did not feel qualified to recommend any specific package. After that, it was on to a sane build and deployment process that involved a dedicated build machine and included QA. After a long time I could start safely making code changes to clean the actual codebase up.
I think you'll see this a lot in manufacturing companies who sell physical hardware. The embedded software is a critical part of the product, yet it's not treated with much more care or seriousness than anything else on the BOM. Night and day difference between working at a company where software _is the product_ vs where software is just one of the many components that goes inside the product.
And that is why one should always ask about these important things before the first day, i.e. during the interviews, before you decide if you want the job or not. Fortunately I'm in a position that the above would be a near-automatic "no" from me.
edit "Near" automatic as there might be a way out if the company knows that they need to improve drastically and has the will to do it. The other response "what were you able to do to improve the situation?" is key.
If you're judging a team being clued in by the SCM, you're doing it wrong. There are great teams that use SVN, and know-nothing teams that use git.
The measure of clued-in should be how well does a technology match the organization's needs?
And dont get me started on the abuse of backups or build systems.
"What's your revision control?" "Git." fine "And Mercurial." Okay "And SVN. And CVS. Some are hosted locally, some are accounts on Bitbucket and GitHub".
"Build system?" "Cmake. And Scons. Depending on which version of the software you're building. And Make. And some bash scripts."
Even green field projects there wind up with significant technical debt in under a year or two.
https://en.wikipedia.org/wiki/Wikipedia:Chesterton%27s_fence
You should resist someone who says, "I don't know why this is here, let's get rid of it!" Only get rid of something if you do know why it's there!
I know this is orthogonal, but please try to avoid these situations by commenting code which people might wonder exactly that of, in the future. :(
As for architecture being the biggest technical debt problem that should be obvious. Architecture is often the most difficult thing to change after the fact.
None of the companies I have been at created a good model system (although one came close, the employee responsible left due to the startup acting like a consultancy instead of focusing on a quality product) - this has often bit companies hard as data structures start mutating due to changing API & changing requirements, making certain things such as offline caching more difficult.
Architectural choice tends to roll into poor time constraints since poor time constraints puts even more pressure on the code base when poor architecture is involved.
[0]: 100k-1M is a very large range. Kudos to them for filtering out small code bases, but a graph of these perceptions measured against code size would have been helpful.
I did not explicitly look at size as this was not one of the original research questions, but a good point. I suspect older systems will tend to be larger (in the domains we studied, anyway). And your point about arch choices being great "early on" can, I think, be captured in the "system age" variable. I guess I'm trying to think of a system that might be young and yet quite large in LOC.. would be an interesting outlier to look at.
Take a look at video game companies. They tend to build up a large infrastructure early in the development of a game.
Keep up the great work! I love to see scientific research being done on software development. :)
1. The system grew alongside the devs' understanding of the problem space. There was never time to change decisions that were later found to be not the best (and the older such a decision is, the harder it is to fix).
2. The problem space changes over time. Either because the outside world changes, or because of scope or scale increases. This means that even good architectural decisions can become bad over time.
Isolated pieces of bad code can be fixes when/if they need to be touched for some other reason. Fixing currently-inappropriate architecture takes a bit more effort.
You try to make the best architectural decision that you can at the time, with the knowledge and resources you have available. Time passes and you learn new things -- maybe the problem changes, or your understanding of it improves, or your understanding of alternative implementation strategies improves. For whatever reason, you can now imagine a new architecture that would be superior, if only it were implemented to replace the old architecture. Now you have technical debt. This doesn't necessarily mean that the best decision now is to pay off the debt by reworking the architecture -- that depends upon a cost/benefit/opportunity cost analysis.
edit:
Tangentially, there's a pretty interesting presentation by Kevlin Henney titled "The Architecture of Uncertainty" [1]. My poor summary: When designing the initial architecture of a system, Kevlin suggests that the team brainstorm to identify which parts of the system have a lot of uncertainty. Each region of uncertainty then becomes a subsystem. Put interfaces between the subsystems that need to be connected. Hopefully you now have an architecture with stable interfaces, even if individual subsystems need to be completely rewritten during the course of the project.
[1] http://www.infoq.com/presentations/Architecture-Uncertainty
Based off of your edit's description, this technique feels like a patch that inevitably fall apart. It relies on the assumption that your team can correctly identify the centers of uncertainty, and that that uncertainty model will continue to apply. Thinking about such things is an excellent idea, but it is not sufficient - uncertainty is, after all, uncertain.
I think in many cases the more important thing is to create an abstraction that allows you to perfectly represent your existing business logic in the most concise way possible. This should let you cut down on the number of edge cases outside the model, and generally simplify the system. Simplicity in specification is important, because it will allows newcomers to quickly understand the inner workings of your code, quickly correlating business logic with real code - if they can understand it, and can work within it, then they will not be tempted to hack around it (which is the root of code deterioration). I strongly believe that human friendliness and understandability should be key design goals in ANY new system, not an after thought.
So long as no one breaks the abstraction, the 99%, day to day changes should be easy. When you finally do hit a case that requires a significant abstraction change, then your concise code will make it obvious that it's outside of your abstraction model, and can evaluate options at that point.
If you are curious about what I mean, I invite you to try a little experiment. For a few weeks (2-4 should be sufficient) tell people, "If you see something wrong with the design, refactor it as soon as you see it. If you need some extra time on your story to compensate for that work, just bring it up at standup and we will modify the sprint commitment".
If your experience is like mine, you will find that the vast majority of people will not refactor the design at all. About 10% of the people will try to refactor something, and will end up trying to rewrite the entire app. They will do more damage than good and will probably give up half way through. If you are lucky, maybe 5% will actually refactor something and be successful.
Because I have tried this many times, I've interviewed people and asked them why they do what they do. For the people who don't refactor, the reason they usually give is: "There is no time to refactor". Which is really odd because they have explicitly been given time. What I have come to realize, though, is that people do not want time; they want absolution of responsibility. If you ask them to make the judgement call, they do the math in their head (unconsciously) and determine that they will be rewarded more and criticised less if they do feature work without refactoring. This forces the decision up to the PM/PGM/BM/BA/Whatever, for whom refactoring has no direct benefit. The result is that refactoring is rarely done, and if it is done it is the result of a large political process.
For the people who try to rewrite everything, training seems to be the overwhelming issue. They pull on a thread and the whole sweater comes apart. For some people in this category, though, giving them carte blanche to decide what to do means that they feel they can finally "do it right". Doing it right in this context means that they can replace all the code that they didn't personally write and therefore don't like. Since everyone on the team only writes a small portion of the code, it means that rewriting everybody else's portion is basically rewriting the app. Again, training seems to help because even if a person's goal is to replace everything, if they learn how to do it piece by piece they can be successful. Also if they do that, they will be required to have many coversations and may eventually learn how to work with others.
Finally, you may get one or two people who naturally know how to refactor well. It is useful to find out who these people are and to encourage them. Unfortunately, this often enrages the "my way or the highway" people. The people who are good at refactoring, if encouraged, will naturally dominate the design of the application. Often these people are suppressed by political means because they are so effective at driving the design. In order to enable these people you will need to make some tough decisions on the business end of things.
Even within "just a web app" one can run into some gnarly issues or odd old code that renders otherwise simple tasks difficult, but since it's only one person working on it you can't really test whether or not it's actually difficult.
This might impact both the dev/manager disconnect as well as imposter syndrome.
My #1 reason to avoid refactoring code is because that code has been there, is battle tested and hasn't had a bug filed against it in months. That is not my first instinct, either. There are lots of things I see that I had written months ago that I badly want to rewrite every time I see it. I have to restrain myself because it's not just the time spent refactoring. It's writing tests (if you're into that sort of thing). It's getting QA to hammer on it some more. It's fixing all the little bugs that you thought you had fixed that you re-broke. It's all the little bugs you've never seen before because this is a new design and you're not perfect.
Most of the time, it's not worth the refactor, even if it does slow down adding future features to that particular area. There are, of course, two exceptions: 1) if you're constantly playing bug whack-a-mole on a particular section and 2) if a section of the code you're working on is constantly changing. I grasp every opportunity to hold a meeting, stand on the nearest chair, strike a dramatic pose and shout "We are rewriting the loading system... FROM SCRATCH!" My project manager then tells me to get down and lay off the coffee, but that's ok, I've had my moment.
On that note, a few things I try to keep in mind: As you say, don't gratuitously change code. You may hate the design, but if it ain't broke, don't fix it. This is probably the biggest mistake that "change the world" programmers make.
Second, try not to rewrite code -- ever. Usually there is no business case (see "don't gratuitously change code"). Even if you think there is a business case, it dramatically increases risk. My rule of thumb: anything that lasts longer than 2 weeks has a very likely chance of being cancelled. If you must rewrite, it has to take much less time than that.
Finally, keep in mind that refactoring is not rewriting, nor redesigning (though it is closer to the latter than the former). Refactoring is transforming the code so that it performs exactly the same function (bugs and all!!!) with a different "shape". Ideally you will have tools to help you refactor in such a way so that you can prove that the resultant code executes in exactly the same way as the original code. With or without the tools, you should have a suite of unit/integration tests that will alert you when you have made a mistake.
Refactoring allows you to slowly migrate code from one "shape" to another over time while not breaking it. People who are skilled at refactoring can evolve efficient design even starting with really badly written code.
Why do you want to do refactoring? While, as you point out, the cost of doing a work-around from a sub-optimal design is low, the cumulative cost of these work-arounds over time can be quite substantial. A work-around introduces complexity to the code. This complexity makes everything slightly more difficult and slightly more risky. It also makes further work-arounds more likely. These work-arounds compound the problem. Because poorly designed code usually has high coupling, problems in one area of the code can manifest in other areas without warning. As the work-arounds increase, the complexity can increase exponentially. As an example of a worst case scenario, I once worked on a project where the programmers averaged 1 line of code per day (Yes, LOC is a poor measure of productivity, but no matter how you slice it, that's just incredibly bad).
My experience has been that teams which refactor effectively outperform similarly skilled teams who don't refactor by a very large margin. Although productivity metrics are impossible, the difference is quite startling. Interestingly, I have also had some experience with teams that have very high test coverage, but which don't refactor consistently (or effectively). These teams also do not seem to benefit from dramatically improved performance. My current theory is to write tests to support refactoring and don't worry about any of the other benefits.
YMMV :-)
Interestingly, I have also had some experience with teams that have very high test coverage, but which don't refactor consistently (or effectively). These teams also do not seem to benefit from dramatically improved performance. My current theory is to write tests to support refactoring and don't worry about any of the other benefits.
That's something I had never considered. Do you have any general guidelines for testing for refactoring?
Great point. They know they've been told they have time but they don't actually believe they have time.
To be clear, there was nothing malicious about it, it's just that many stakeholders only give their short term needs as input, not the long term strategy that's discussed behind closed doors in the board room. The big problem here is that non-engineers don't get that some strategies don't simply add to the problem space, but fundamentally change it.
"Why the fuck didn't you tell us sooner?" is one of the most common phrases in software development.
I like how `debt_ceiling` supports `TODO` or `# TECHDEBT` tags.
When I think of technical debt, there are some kinds that don't seem easy to automatically measure: "we made an assumption 6 months ago that turned out to be quite wrong, but we haven't had enough time to go back and rework it yet". This could manifest as poor choices of data structures, or an entire system/subsystem that turns out to be nonsensical given increased understanding of requirements. People who understand the problems can manually tag the code, so it's good to see tool support for these kinds of annotations.
The context in which this study was done is quite different from an early stage startup. This is not surprising since the SEI tends to focus on big engineering solutions to big problems [which is not to say that its insights and guidance aren't applicable]. But it is important to realize that they focus on engineering in situations that are relatively stable over the long term (29% of systems studied over 10 years old), and almost 1/3 of the systems studied were embedded where changing the software is particularly difficult and throwing more hardware at a problem is not cost effective.
What costs the most is architecturally unsound code. It's actually not really the code per se, it's the abstractions and how they fit together--very specifically, to what extent the abstractions cohere, to what extent they are coupled, and how narrow are the interfaces. Give me a code base that nails these qualities (narrow interfaces, minimal dependencies) and all else (duplicate code, inelegant code) is in comparison NOTHING to deal with.
So the cost of code is almost entirely related to its _architectural soundness_ (to what degree it's coupled, its interfaces are too wide, etc.)
The biggest and prevelant fallacy of our industry is this notion of technical debt. Because the notion implies that it's somehow necessary. It's not necessary.
The ability to build and evolve architecturally sound code is a function of skillset. If the skillset is there, there is no necessary extra cost. In fact, for people that have this skillset it costs MORE to depart from instincts and couple code, create wide interfaces, etc.
When a developer says 'It will take too long to do the right thing,' (i.e., build architecturally sound code) he/she is saying, 'I don't have the skillset.'
I know all the objections here (it takes time to understand your use cases, etc.), but the objections nearly always miss the fact that architectural soundness and things like code scope/flexibility are ORTHOGONAL properties. You can have very specific code that does very specific minimal-viable product type things ... but this has nothing to do with whether the code is architectural sound or not.
As long as we continue to think of techincal debt as something that's somehow necessary and something to be negotiated we will always be missing the point. The point is that we need to produce workers with the right skillset. There are so few people that have it--which is entirely a failure in our academies, educators, training systems... To keep talking about technical debt and not about how to acquire the architectural skillset is to keep chasing our tails.
Apologies if this is too meta, but that is the most interesting part of your comment for me. It conceals a massive assumption which all devs make, resulting in very different conclusions.
What IS technical debt?
My own preference for the answer is Uncle Bob's description, which is this: technical debt is any production code that does not have (good) tests.
But regardless, any attempts to address technical debt must necessarily begin with a common understanding of what technical debt actually is.
I've rarely seen teams agree that understanding and I think that contributes to their problems.
Any feature of the code or tech stack that increases maintenance/operations cost compared to alternatives.
> My own preference for the answer is Uncle Bob's description, which is this: technical debt is any production code that does not have (good) tests.
That's certainly an example of technical debt.
> That's certainly an example of technical debt.
Agreed, it is not the only example, but perhaps it is a good one, as that is a particularly important form of debt that makes the code harder to safely change. I.e. it is a form of technical debt that makes it more expensive to pay off other kinds of technical debt.
Curiously, Michael Feathers has a similar definition of legacy code [1]:
> To me, legacy code is simply code without tests.
Tests are necessary but not sufficient. (Particularly, tests embody
I'll be charitable and say this may be the case part of the time. But as a general argument it is bollocks. In the vast majority of production systems that survive for any amount of time there will be requirements changes, and sooner or later an engineer will face a choice of shoehorning an attribute in somewhere it doesn't fit naturally, versus refactoring to more appropriately model the new business universe. Saying that this won't happen if your skills are sufficient is a no-true-scotsman argument—it's all tradeoffs, and no matter how good you are at anticipating change, sooner or later even the best architect will be blindsided by some out-of-the-blue business requirement.
The "choice" your engineer faces to shoehorn a feature in [1] versus implement it in a way that optimizes for soundness (decoupled code w/ narrow interfaces) is not a choice if the skill set is there--i.e., if he/she has trained and practiced the right skill set.
[1] (By 'shoehorn' I assume you mean implement it in a way that leaves the code base in an unsound state.)
This is such a vague statement, I have no idea what you mean or how it relates to our discussion.
My argument is this:
(1) The most costly issue with a code base has to do with its architectural properties. How architecturally sound it is [1]. Most other issues that we tend to categorize as technical debt (like code duplication or verbose code, for examples) pose such tiny cost in comparison to the cost of poorly architected code [2].
(2) Architectural soundness is an objective property of code independent of the code's generality, flexibility, etc. Again, it's the degree to which its components and its interfaces narrow.
(3) There exists a skill set that understands, recognizes and produces architecturally sound code. [3]
(4) Given an architecturally sound code base and a set of features, a skilled person can implement the features without violating the architectural soundness of the code base AND do so without having to pay for it in time-to-delivery. [4]
I think I'm being fairly clear in my assertions here. They are based on my long experience. To disagree is to pose a different theory. Do you care to elaborate on what you mean by 'crackpot territory'?
---
[1] More specifically, the degree to which its components (modules, classes, functions) are coupled and their interfaces not as narrow as they could be. This is an objective property of a code that has clearly delineated components (i.e., you can quantify dependencies, interface sizes, module sizes.)
[2] Yes, this is a claim based on my 20 years of experience working in myriad code bases. How to distill and prove this, formalize the argument, etc is probably work to be done.
[3] This skill set doesn't come cheap and requires proper training, and currently our industry/academies aren't producing it.
[4] Again, this claim based on my experience and yes there is probably some good research opportunity here to formalize it.
I like this definition, and I agree you could quantify this into a useful measure. However the optimal architecture according to this measure is a function of the problem domain. You split the modules and define the interfaces to maximize the encapsulation and minimize the interfaces.
So what happens when the business rules introduces a new cross-cutting concept? The basis of the architecture soundness is now in question. Do you define a new module and add n new interfaces? Or do you split the concern into the existing modules, fattening them in the process, and widening existing interfaces as well?
I agree with this. But domains typically evolve incrementally -- and using the skillsets I mention you can evolve your code accordingly. [1]
There is then the question of these cross-cutting, earth-shattering domain changes you mention. I'm not convinced my argument doesn't have a play here but regardless if these kinds of major domain changes are happening frequently enough to matter in this discussion then I think there's a different business-level problem going on.
It's hard to talk abstractly about this for so long--until of course someone gets to building out these ideas more formally. Getting into concrete cases would be more fruitful.
---
[1] That you can without cost tradeoff is my argument. Today these skills are so rare so you don't find orgs/codebases that do this properly--and therefore we have this fallacy that because it's prevalent that it's somehow necessary. We don't realize it's really a skills problem.
According to the author call options are a better metaphor for what we call technical debt but admittedly a more difficult one to understand: anyone who knows what a call option is raise hand.
However, change is imposed on it inevitably from the outside, so instead of making large-scale changes to its architecture, it tries to keep up with the market by making thousands of small hacks. The original architecture was not designed to cope with this, so it has become hack upon hack upon hack. I should be employed (and paid above-average) in this jobs for decades to come.
That's my bit of advice to you on that, as it reads a bit like you're very comfortable and expecting it to stay that way for "decades to come".