Amazing as it may seem after all these years, there are still junior developers in the world.
A few weeks ago at work we had a talk where senior developers (including me) were invited to spend around five minutes each talking about our personal software development philosophies. The idea was for us to share our years of experience with our more junior developers.
After the session, I felt that it might be valuable to write my own thoughts up, and add a little more detail. So here we are.
This listing is a little miscellaneous; it isn't intended to be an exhaustive exploration of the way in which I develop software. Also, if you are a senior developer already then obviously you might already be familiar with some of this. Or disagree! Software development is a famously subjective field. See you in the comments.
It's generally pretty well-understood that the ground-up rewrite can be an attractive and extremely dangerous prospect. The standard advice when it comes to ground-up rewrites is "Don't, ever". But I want to take a step back from that.
By the time the ground-up rewrite starts to seem like a good idea, avoidable mistakes have already been made. This is a scenario which you can see coming from a long way out and you can, and must, actively steer away from.
Warning signs to watch for: compounding technical debt. Increasing difficulty in making seemingly simple changes to code. Difficulty in documenting/commenting code. Difficulty in onboarding new developers. Dwindling numbers of people who know how particular areas of the codebase actually work. Bugs nobody understands.
Compounding complexity must be fought at every turn. Alternate between phases of expansion (new features) and consolidation.
Of course, a ground-up rewrite can actually work. It might even be a better choice that the alternative (persisting with your existing technical debt-laden swamp of code). Equally, it might be that neither choice will work — the project is doomed, and you're just choosing how it dies. The point is that there is inherent risk to this situation... but the situation itself is avoidable, and that risk is avoidable.
There is a famous adage in software development — actually, thinking about it, it might originate outside of software development — which goes,
The first 90% of the job takes 90% of the time. The last 10% of the job takes the other 90% of the time.
This is mildly amusing and absolutely factually accurate. Having understood this, it is entirely possible to correct for it.
Writing the code, once, and getting it to work, takes a certain amount of time. Once you have done this, you need to understand that you are about half done. Polishing code up to a suitable level of coherence and maintainability, proper handling of edge cases and failure cases, unit testing, integration testing, usability testing/demos, "last-minute" feature changes, performance, serviceability, documentation... all of these things can take immense amounts of additional time, and they are also part of your job.
Many of these things are theoretically skippable. But in practice when you skip these things you end up with a shoddy, incomplete feature. And nobody is ever going to come back and finish the work "properly" afterwards. There is always more work. Do this three or four more times and you have a shoddy product.
Also, the writing of the code itself will throw up unexpected roadblocks. It is advisable to try to discover these roadblocks as soon as possible.
And if it magically turns out you don't need the extra time you planned for? Great, time to implement some process improvements! Or pay down some technical debt (see above)!
Sometimes there's a particular thing which developers on a project should all start doing or stop doing. There's new best practice. There's a new tool we need to use consistently, everywhere; a new mandatory header on every source file; a check everybody has to run; a method which we've collectively decided is no good to use (either an internal method or a third-party API). When this happens, there are two ways to get the developer base as a whole to change its behaviour:
Add an automated test which fails if the guideline is not followed. Or, if we can't fix everything everywhere all at once, add a ratchet. Fail fast, with a polite and instructive warning, if the right thing isn't done, or better yet, automatically fix the problem. In general, enforce best practice mechanically.
Automation isn't a perfect solution or a universal solution or a universally appropriate solution for things like this. There are plenty of softer requirements and abstract technical requirements which can't be automated, and it's possible to get really annoyingly strict by introducing too many arbitrary rules, and motivated developers can usually circumvent automation by various means. But if you find yourself telling people over and over again, "You forgot to do X, please remember to always X", maybe it's time to automate X?
Nobody cares about the golden path. Edge cases are our entire job. Think about ways in which things can fail. Think about ways to try to make things break. Code should handle every possibility.
What if the request fails, or stalls forever, or sends back one byte per second for an hour? What if the table you're showing has a million rows? A billion rows? What if the name has a slash in it, or trailing whitespace, or is a megabyte long? I don't believe you when you say that you can prove that that string can't be empty!
If you budgeted your time properly (see above), you have time to go back and see if you can do better. C.f. the old chess adage, "When you see a good move, look for a better one." And another difficult-to-source quote, "I apologise for writing such a long letter, but I didn't have time to write a short one."
This means well-defined interfaces and minimal side-effects. Code which is proving to be difficult to unit test is probably not properly encapsulated. If you find that:
assert.equal(func(a, b), result)then that is a problem with the code under test, and the code may benefit from refactoring.
Some code seems to work correctly by accident, because the circumstances which could cause it to receive bad inputs and fail are ruled out by the structure of the other code surrounding it. I dislike this. Although technically the code may be free of bugs, restructuring the other code is now difficult and dangerous.
This is particularly true for security issues, or the theoretical absence of security issues. It doesn't matter that all of the callers to this particular internal function are trustworthy right now.
I had one other thing for this list but I don't remember it right now.
Discussion (12)
2025-02-03 19:31:33 by Richard B:
2025-02-03 19:33:18 by Richard B:
2025-02-03 19:59:54 by tyler:
2025-02-03 20:17:15 by Aybri:
2025-02-04 17:01:14 by Adam:
2025-02-04 20:28:42 by Ian Z:
2025-02-07 04:05:11 by Ravi P:
2025-02-10 21:04:15 by phyphor:
2025-03-02 06:52:41 by mlb:
2025-03-12 01:54:50 by Gavriel Plotke:
2025-08-11 14:22:26 by plop:
2025-08-21 22:48:19 by qntm: