Ask HN: Have you ever inherited a code base you thought was well done?
I see and hear a lot of complaints around inheriting code bases that are less than stellar. If anyone has, I'd love to hear about cases where you inherited a "good" code base, whatever that may mean: awesome test coverage, good documentation, solid organization, consistent styling/formatting, abundant best practices, whatever!
150 comments
[ 4.5 ms ] story [ 238 ms ] threadafter a sufficient time spent meditating on the construction techniques of that project’s chesterton’s fence, most codebases i’ve seen become much more reasonable. Once i’ve spent enough time using it myself instead of just reading it, design decisions or organic evolutions start to make sense.
A good-faith humility is a good attitude to have as a reader, but it only really comes with professional maturity. Un-curling the sneering lip most of us seem to pick up in our late teens takes, in my experience, about a decade in a relatively attentive person.
some are still truly horrific, but they’re relatively rare.
The list came out of a COBOL "DBMS" system that had a 64kb table limit. They had more names to print than that.
The stack of hacks consisted of scripts that ran trough all the tables of the "members list" DB (multiple floppies were doable, by the time i got it they had a hard drive), creating text "dump" files of the bits that went to the printer; more scripts to assemble those dumps and reformat them (in GWBASIC), and finally a script that fed one record at a time through the pretty printer formatting program and printed it out.
My contribution was figuring out a way to feed the "pretty printer" multiple records per run; instead of invoking the chain once per record. saved days for the entire print run.
It was a horrible stack and it wasn't fun to work with and I cussed the people who had implemented it; however: given the constraints when it was built and the resources of the people using it; it was incredibly cool. Until I saw it I'd have said it wasn't possible with that collection of parts, but it functioned as required and did so for a decade. Eventually it was replaced with WordPerfect doing a mail merge operation; the people wearing the name badges complained that the font wasn't as pretty.
[1] https://en.wikipedia.org/wiki/The_Print_Shop
Past me however, is a total c##t that seems to take joy in solving their problems by making them a massive problem for me when the chewingum and dollar store masking tape inevitably fall off.
I do this because I received a project with this note, and it stuck with me as an excellent idea; a letter to future explorers.
Yes!
This is what comments (and READMEs etc) are for. Not explaining the what, ie how your programming language works (unless you're doing something most engineers wouldn't know). But explaining why they were made.
I think this is actually one of the under-rated most valuable aspects of github (and probably other similar systems), things it's changed the most -- that everything has a URI that can be linked to, including in code comments. And a reason I could never deal with "just git and email patches" without a web UI that gives everything a URI that can be linked to from text.
I especially like leaving links to issues in third-party open source dependencies. If someone comes along later and wants to change the thing that was a workaround to a bug -- they are easily started on the trail to figure out if the bug has been fixed, in what version of the dependency, etc.
It wasn't that the code itself was badly or well written so much as the concepts of how each process was isolated from each other, the communication protocol was well established and the external dependencies were kept to a minimum.
Data structures were chosen for ease of understanding rather than (run-time) efficiency which was the appropriate choice for this application. The application(s) relied heavily on various other scripts and the operating system to establish a (secure) network communication, offloading a lot of the complexity from the application to the operating system (where it belongs, in my opinion).
I recently update/ported the code base to work on more modern hardware and, besides some minimal updates and fixes, it worked well.
The code base was 10+ years old and mostly written in a combination of C and shell.
Actually once I was lucky enough to join a company where the code was written well, had good test coverage, the team implemented good code reviews, required tests on bug fixes and features, etc. The pandemic was the five hundred year storm to their leveraged business model (hospitality business targeting business travelers) and they folded within months.
At the next job I created the backend code from scratch and was the primary maintainer. I left it in pretty good shape with good test coverage. There were plenty of things I'd have done differently given the chance though. Hopefully the next guy doesn't curse me.
At the current job things are a mess again. I'm working on improving it. Git blame has me at having modified/added 8000 lines in the backend Python code since I joined three months ago. Slowly digging out of the hole as it caves in around me.
For about a week my daily standup would be something along the lines of “I deleted 34,000 lines of code yesterday.”
In the end I think I threw away about 200k lines out of 350k.
In the span of the next few months I think we ended up identifying one method I’d removed that was still in use.
And qu’elle-surprise, the team’s velocity picked way up, people were suddenly way more willing to make bigger changes or even refactor, etc. People just really needed to get picked up out of the hole far enough that they could see the light and they ran with it from there.
It never became _good_ code, but it became much more manageable. Even if you can’t justify good code for the sake of good code, code good enough to actually be worked on is a must.
At work, not so much, as it's mostly very rushed, badly designed software.
What's stopping you in improving the situation? Time pressure or unwilling colleagues?
Not everything was perfect, but it was much better than the code changed by future generations which tried to mess it up with almost every commit, in the name of "it's good enough", "consistent style is not needed as I can still read the code", "what tests?", "we can refactor later"...
Every variable and function was explained in a series of physical manuals in excruciating, and up-to-date, detail. The manuals had index lists by name, function, type and concept, making it ridiculously easy to find exactly what you were looking for. The documentation felt almost like reading Knuth's Art of Programming. It explained not just how a function worked, but also the dependencies and how they worked on that particular hardware, including pieces of the FORTRAN standard library.
On top of that nugget of most people's fantasies, there was actually a test suite! It wasn't written by the original authors, and had been pieced together over the years. But it was a testsuite for code running on a mainframe the size of a small room. Since when do you ever get tests for code written in the 70s!?
Working for CommBank was hard - the standards for absolutely everything that they have and do are A-grade. A single complaint from a coworker or customer can land you in front of a review board. But the work they produced, at least what I saw, is absolutely worth it.
Holy crap. Not sure whether I would love this or be destroyed by it haha.
I think their inexperience and lack of hubris made them go to a lot of effort to be idiomatic in a language they were using for the first time.
1) Working (professionally) on a project that happens to be open source: https://github.com/metabase/metabase/
2) Coming to a Rails project mostly written by a very senior 7-person team. There was still a fair amount of jank (mostly from seed-round assumptions that weren't holding up when I joined after the Series A) but it still followed The Rails Way and nothing was too gross. It also helped that everyone important was still at the company and available for questions.
PS on (2): Seven of those original eight dev are now gone, including me; the median years of experience has gone from 8-10 to 0-3; the team size is at least 40 and I think more; and my understanding from friends still at the company is that the codebase is in general a flaming mess.
The project lead was incredibly impressed that I was able to make a solid contribution check-in on my first day :-)
First was all the scaffolding/framework and inter-process/server protocol code was meticulous and beautiful. As a young c/c++ favoring engineer, I was very impressed and my design thinking was influenced a lot by this, for a long time.
Second, there were these deep algorithm implementations for digital image processing pipelines and also dicom data protocol implementation stuff. These "plugins" or "processing elements" were heavily optimized to squeeze out every last microsecond out of the start-to-finish execution wall time. So the code was hard to follow just by reading. Also, there's no way to understand this code without knowing the domain knowledge (studying the Matlab implementation of the image processing algorithm – to understand that required having a basic theoretical understanding of signal/image processing concepts).
But this was great as different engineers/teams could work on different deep algorithm processing elements and the framework/scaffolding ensured there were no leakages or undue blast-radius of any messy bugs.
A decade later I found myself staring at a very large php codebase. This codebase had a lot of sprawl but no depth. It was messy (bad idioms, wasteful of resources, functionally buggy etc) but it was easy to read and understand. PHP + the framework/scaffolding we had was very forgiving of these mistakes. The application would continue to chug along even with a lot of warnings and some data induced errors. It took some mental attitude adjustment to not lose it every time I saw crazy stuff like 3-level nested for-loops that would unpack an array of huge serialized objects and iterate over their elements only to not use the results for any final response rendering at all.
I work in PHP every day, and this is something I've picked up from seeing other resources online. PHP with its automatic casting and loose type comparisons is forgiving. This would be great for getting things to run quickly and making it easy for early web development for a beginner, and I think this has a lot to do with PHP's success and ability to get new developers interested back in the late 90s and early 00s.
The issue with this is it encourages bad behavior if there isn't a style guide for the team in place. Sure, we can pass around a string representation of our integers and it doesn't matter because PHP will handle casting for us whenever, but this leads to sloppy and hard to read code in my experience.
Modern PHP has leaned towards a more powerful type system (relative to its previous). I'm really happy with the static typing that the language has added in and I think that's a huge reason for a lot of the old gross PHP code.
Another one is the easy intermingling of server code and HTML. Again, this is powerful for fast and newby development, but ends up introducing complexity and mess. Writing a SQL query inside of a loop inside of a <table> tag? You can't tell me that isn't at least a little confusing to look at.
Fine language with a good trajectory, and it's legacy helped shape the web, but damn can you get your hands dirty.
"good" is incredibly subjective, and subjectivity is temporal in nature. there been times when I thought code wasn't"good" at the time of inheritance, but several years later, found appreciation for it. Perhaps not enough to consider it good...but some appreciation.
I've tried to let go of classifying code as good or bad, or any other subjective means.
Non-exhaustive list of things I'm more concerned with these days:
- How long does it take for a new hire to onboard and be productive?
- How quickly are we able to respond with bug fixes?
- Do we have enough test tooling to have high enough fidelity coverage to say we've implemented something, or that an issue is fixed?
- How observable is it in production?
- How reproducible are any issues?
- How accurate is the documentation?
- Is there enough test coverage to be able to reimplement a portion with confidence?
You can probably take all of those things and distill it down into a "good/bad", but I think in general it's better to look at specific concerns that are managed over time.
I'm less concerned about the current state of the code, and far more concerned with how easily I can change the state of the code without incident.
This is great, it’s the most concise definition of clean code I can think of: how easily can I change the code in ways that the system as a whole changes only in ways I intended.
I think this has to do with a common mindset among developers. If they don't immediately recognize familiar patterns and structures then it's the fault of the code. Many folks value readability and believe that code should be written for other humans to read and understand. Yet what we consider readable varies greatly among individuals, programming languages, and communities.
More experienced programmers won't be so easy to jump to conclusions. They may realize that it takes time to understand why code is written or structured a certain way. They recognize that it will take time to learn and appreciate the code.
However experienced programmers also develop a sense of taste and style. They build opinions based on experience and if they see a pattern in use that they associate with negatively then it's likely they will not have a good opinion of the code base.
Less experienced programmers are trying to build their sense of taste and style and will associate with whatever they perceive makes them superior. They often have an immediate and strong reaction to a code base. Their opinions and feedback are often couched in absolute terms.
Personally I've inherited great code bases. One of my favourites was a messy, old C++ web application written in the 90s by someone without much experience at the time. It didn't use any standard libraries, had no tests and documentation was non-existent. It used the file system as the database storing XML files all over the place. A single-threaded CGI application: something I wasn't unfamiliar with.
You would think I would have held my nose while dealing with this code base. Yet I consider it a good one because the team that came before me did a lot of work to wrap this monstrous code base in Python and started writing tests for it: a lot of tests.
Those tests enabled them to start synchronizing the data the application normally stores in XML files into a Postgres database. When I came on I had a completely different idea of what legacy software was. The engineer handing the project over to me gave me their dog-eared copy of Michael Feathers' Working Effectively with Legacy Code.
I like that code base because I learned a lot from it. I kept adding more tests and started adding more functionality and replacing the old C++ bits slowly but surely. Eventually we were able to get rid of the XML stuff, the email system, and replace it with Python code. The application was running the whole time and making the company money. It was a great learning experience.
One of the qualities I've come to value the most in an engineer is tenacity. When someone inherits a code base and can roll up their sleeves and make it something better than it was before: that's someone I want to work with.
Too many developers raise their hands, complain that this code is terrible, and suggest re-writing it. Or they burn out and find new jobs else where. Me? I like to stick around, figure things out, and make them go to 11.
I get that. I also inherited some very awful code in my first job (almost 20 years ago) by some developers who misunderstood basic concepts of programming. They had such fundamental gaps that I (right out of college) was able to clean up their mess in a few hours.
> Too many developers raise their hands, complain that this code is terrible, and suggest re-writing it. Or they burn out and find new jobs else where. Me? I like to stick around, figure things out, and make them go to 11.
I agree completely. I get the best job satisfaction making things go to 11.
So I don't think that a good codebase does any one particular thing well, it just avoids the bad parts of bad codebases. Via negativa in practice.
It's easy to spend a day with a codebase (that others spent years writing) and call it "bad". I'd argue it even feels pretty good to take that stance of superiority. But you're viewing it with literally zero of the context of the time in which it was written. You see none of the constraints, none of the pressures, none of the alternatives presented in the moment.
Particularly for a young or small company, if you're "inheriting" a codebase it's because it's existed and been in operation for a while. Yes, it may still be bad. But I would advise taking time to consider whether it's actually, within the lens of yesterday (or 2 years ago)... good?
Or was lucky enough that all the choices they made did not blow up yet.
I totally understand the value of technical debt. But I have also seen in the wild cases where people thought the codebase was great simply because they hadn't run into cases where its rotten core would be exposed.
Yet.
For any mid engineers who run away from spaghetti, I’ve learned identifying and solving that spaghetti problem can be a huge leap forward in your career towards management/leadership roles provided you find the right company and are able to solve the spaghetti issues.
Other than to soothe egos, this almost never matters, because the person inheriting someone else’s code doesn’t live 2 years ago and can’t be blamed for not having the prior context (establishing that context is the job of the original author and is why documentation matters).
If you find yourself in the thankless position of having to reverse engineer intent and especially if you have an unsympathetic manager ready to lecture you about how you should magically read the mind of the original author, start looking for a new job and hack/slash away as much of the old code as possible to do the job that you’re asked to do.
For example, I've worked on a huge C# product that has been developed in various epochs by up to 100 people at a time. When I was working on it, it was around 10 years old. Certain parts of it were in good shape, others had stuff like a 5k+ line OnPaint() method for a table cell that was doing data processing in place and had stuff like "if ((value == -2) && (type == someTableType) && (userOptions.SomeFlag == SomeEnum) && (someOtherColumn.Value == "something")) { value = paintCheckBox(enabled) }" and such.
Of course, that code was doing what it was supposed to (at least in general), and it had gotten to this state during a really bad delay where bug fixes/features had to be churned out to reach an important release date - so in some sense it was "good". But in the sense of being possible to change without a major re-write (of that particular area I mean) or without introducing 3 bugs for obscure combinations of features, it was really bad code.
But there is another context in which we might evaluate code, something like, “how far the code is from ideal, where ideal is the state the code would end up in if we could push a magic button to refactor the entire codebase, for free, to perform identical functionality and reduce both operational risk and the cost of future development”
Or, more succinctly: how clean is the codebase? Clean code has a cost, and sometimes it isn’t worth paying. That one off bash script that runs once every few months perhaps isn’t worth tidying up. I think the author is really asking about clean codebases.
I recently inherited a code base which was developed by some devs-for-hire while the company started building up the team to take over (including me).
I met the last remaining dev-for-hire for the handover and got the impression that the company paid very very close attention to how many hours they billed and which meetings they were allowed to attend, and 0 attention to the code they produced as long as it worked.
Thankfully, the code was actually working well enough - and the thing that I appreciated most of all was the fact that at lots and lots of places in the code, there were frank comments like
So incredibly helpful. No Chesterton's fencing around, just clarity about the parts of the code everyone knew was just around to do its job, then get rid of.That’s an interesting way to put it. I think that might be a good rule of thumb to help future maintenance: Avoid unintentionally erecting Chesterton’s fences.
This is a great rule of thumb. I often find I unintentionally do that all the time and then I have to figure out why I did something I did. Trying to avoid that with good comments, documentation, etc. earlier in my career would have been a great practice.
I find I often think something is so common-sensical that I would never forget and then come back a while later and have completely forgotten.
As in, "put some traffic cones around that hack" meaning make sure there is a comment or appropriate error message or something that will make it clear to posterity that this is a known temporary sketchy situation, not an accidentally load bearing wall"
Another (in-house, salaried) contributor to the project I took over added a error message (in a very specific error handler for exactly one REST endpoint), and I quote
When you operate software, you realize all the little things a developer can do to make the operator's life much easier or harder. I always thought this was the "true meaning of DevOps" -- thinking of how to make it easy to operate the software at dev/design time, as if it were an old school shrinkwrapped application like Windows 95... but instead it got turned into writing terraform or something haha.
Because if he's the only one that understands the error message, he's the only one that can fix the problem.
That's how you get credit with the suits if something breaks.
No one ever notices a stable application. A programmer fixing a problem that no one understands? That must be a very smart person indeed.
I'm being needlessly cynical at this point. This was just one person at one company. Overall, I do think that given the right incentives, what you describe can happen. But it does not someone in leadership to care. And that's a hard sell to me.
survival is far too low of a bar. We want delighted customers, low churn on employees, low minimum bar of required IQ to understand it (easier hiring, less stress), inexpensive modifications + improvements.
Survival is only sufficient for cash strapped startups on borrowed time&money because if survival fails, then the future no longer matters.
But as soon as the future seems probable there is _much much_ better aims than just surviving.
The next target then is acquisition or an IPO which is then followed up by quarterly earnings. It’s always survival first unfortunately
Correct. I sometimes invite coworkers to partake in a thought experiment where they anthropomorphize the company (and/or codebase) they work at imagining, if it were a person, what state of health would it be in? This usually engenders a chuckle or two of some sort as they picture someone whom, though they are surviving, is otherwise in seriously poor health and does well to illustrate to them that if they were this individual they would likely be striving to significantly improve their quality of life and not just merely maintain status-quo of still having a pulse being the measure of success.
Well put! When I was younger I would call codebases bad after a couple days of working with them. Now, it takes more like a couple months to fully form my opinion, although it's less about "Is this codebase good or bad?" and more like "What refactoring opportunities do we have to make this more maintainable and which ones make sense (from a bang-for-the-buck / prioritization perspective)?"
I’ve seen it time and time again, with myself, colleagues above and below my grade, and devs I’m mentoring.
Chestertons Fence is a very important concept to have deeply engrained in your mind.
Of course, with experience, you get better at designing solutions to add some "optionality" in case things change, but it's a trade-off that you can't always make, nor should you always make.
I think that's underselling the problem. A bad codebase is a codebase that isn't what it should be. Just because there are reasons, even legitimate reasons, as to why the codebase is what it is, that doesn't mean it isn't bad. Constraints can be bad and bad constraints make a bad codebase.
In my opinion, thorough documentation is a minimum. If a codebase doesn't have documentation, then it is automatically bad.
Software/programming/whatever you want to call it is about three things: (1) instructing a computer, (2) communicating between humans, (3) encoding knowledge of a domain. If a codebase fails at any one of those three, then it is a bad codebase. Too often, people only view a codebase in the lens of (1), in that if for the most part it does the thing it's supposed to, then that's enough.
While this is true and compassionate, I think bad code is still a bad code. It'd be dishonest to call a bad code a good code.
I agree though that from time to time circumstances simply wouldn't allow developers to take the time to write good code and/or address tech debts. This problem I feel would need to be catered at management level. If companies were to attract great talents, they would need to maintain certain standard of code quality and empower their developers to do clean ups/refactors every once in a while.
I inherited a codebase where the backend is written in Kotlin (using Micronaut) and the frontend in react
Both the backend and frontend are very clean and i learnt so many new cool things just by reading the code.
The code is so easy to follow and understand, and the architecture is very nice
The frontend's consists of react functional components that are are written in a way that makes them very reusable and configurable; each component can be extensively configured with props, making it very rare to have to create new components
The backend is structured into independent microservices and it is therefore very extendible and the microservices themselves are small and easy to modify
I think the reason it was good is that nothing was too integrated. You had of course scripts who integrated multiple others (mostly Ansible roles tbh), and that could be complex to understand, but everything else was great.
So: keep everything small?