It’s far too often that I see people shying away from newest technologies in the spirit of backwards compatibility. “We can’t move the minimum PHP requirement to 5.5 because we have 50% of our users on 5.4 still!”, they say. “There’s no way for us to move to Guzzle 4+, our back end is built on version 3 and it would take too much time and money”. I like the common argument from WordPress the best: “We can’t go full OOP and logic/presentation decoupling, because most of our users are running shared hosts with PHP 5.1 or don’t know OOP and/or MVC”.
Legacy Code – a big NO
This might come out controversial, but I firmly believe there is no room for legacy code in modern systems. Allow me to elaborate before you sharpen your pitchfork and light your torch. What I mean by that is: there should be absolutely zero reason to keep implementing the functions you’re adding to the new version retroactively into the old version, just because some people are still using it, even if the people using it are a vast majority.
To clarify: bugfixing legacy versions until their long term support contract runs out or you feel like it if you’re in charge, yes. Adding new features you think up for version X into version X-1 in order not to make the X-1 users mad, absolutely and 100% not. Likewise, adding X-1 code into version X just because it can “serve the purpose” should be illegal. If you’re still charging people for X-1 and basing your upgrades on that, your business plan is bad and you should feel bad.
Who am I to spout such nonsense, though, right? I’ve never had to maintain a large project with stakeholders and boards to please, a project that moves super slow and makes everyone happy as long as it works, no matter the fact that it could, potentially, work 100 times safer and 1000 times faster, right? Not exactly. My biggest baby was a big publisher site with a complex back end, built on ZF1. If you’ve ever done anything in ZF1, you know what a vortex of painful antipatterns it is. When the application started showing signs of deterioration due to increased traffic, I rebuilt the front end of the back end (the most heavily used part of the app) in its entirety on an ajax interface and API calls, lightening the load drastically and buying enough time to rebuild the entire suite of applications we had on the only thing the higher ups allowed – Zend Framework 2. If you’ve done anything on that, you know it’s a slightly less dense vortex of antipatterns, but still a vortex of antipatterns and bloat – but what I’m trying to say here is – huge upgrades and total rewrites can happen, if capable people are behind them. If all you’re doing are agile meetings and brainstorming, there’s no amount of LTS contracts that can stop you from looking stupid in five years.
Even if you’re doing free and/or open source work, you shouldn’t break your back for X-1 users, because you’re only doing them a favor by doing a major version increment, and with it, a major upgrade with a potential BC break. They should either adapt, or wither away.
So why should we exile legacy code from modern systems?
The Neverending LTS Curse
By taking the “support everything for as long as we can” approach, you’re burying yourself in a bottomless pit and looking at stretching yourself so thin several years down the line when you find yourself having to maintain four different versions, you’ll be banging your head into the wall wondering why you didn’t cut the V1 and V2 users loose when you still could have. In an attempt to maintain a bigger audience, developers often go out of their way to help users of past versions, for the sole purpose of keeping them around. This is also why WordPress is, in its current state, such an unfixable mess of amateur code. Don’t let yourself get chained to old versions.
These users are dead weight and should be discarded, no matter how much money they bring you. Give them methods of transition and move on – if they’re in any way capable, they’ll catch up in no time. If not, they’re not worth it.
By supporting older versions for too long, you enter the WP curse in which older versions, versions so bad they’re cringeworthy, require ever more manpower and effort to fix. This manpower is better spent building new versions and hiring developer advocates to help users transition.
You’re alienating and negatively affecting advanced users
The last time this desperate legacy grabbing affected me directly was while installing a CMS that proved particularly difficult to install in a Vagrant environment – not only due to symlink issues which are, by now, widely known (even to the creator of Vagrant), but due to them including a legacy version of the CMS inside the main CMS, because they share some installation properties, and the back end wasn’t fully rewritten into the new version yet. Why move onto a new version at all then? Why rush things if you’re clearly so far from ready?
By rushing the new version, they ended up with a sort of hybrid that’s neither here nor there – a Frankenstein’s monster of legacy code that still kind of works, but badly, and new code that has potential but cannot reach it without the mess that is the legacy code. While this approach does make the job of development companies who are still stuck in the 1990s easier, it makes the job of advanced users much, much harder. By catering to a crowd that should, by all logic, be extinct, you’re alienating and negatively affecting advanced users even more.
You know how it goes: don’t waste too much time trying to do the little details in legacy browsers. Nobody using those browsers will notice anyway. The same applies for users of libraries or content management systems – those who care about the legacy system won’t care about what you’re introducing into the new one, so you shouldn’t sweat blood over them more than necessary.
Failure sometimes ushers success
Of course, sometimes it just isn’t possible, and these exceptions are rare and valuable learning materials. One curious case of versioning and BC breaks is Python. Python is an awesome language. You can build almost anything in it. It won’t work as well as it could in a language built for that purpose, but that’s the nature of jack-of-all-trades types – they can do anything very well, just nothing flawlessly. When Python 3 came around, it introduced some BC breaks, preventing Python 2 users from a simple transition.
Most excuses were “There aren’t enough Py3 packages out there yet” and “We have too much code in Py2 to rewrite it all” – to which I say “Build what you need yourself” and “Poor programmers, being forced to program and all”, respectively. Granted, there were some valid arguments, and those tend to show up in projects that are so absurdly large, but the Py2 vs Py3 issue actually resulted in something else – the “Python Rift”. By the time Python 4 rolls around, the people who so vehemently refused to transition to version 3+ will still be stuck in Python 2 and the BC shift will become even greater for them. At that point, they might as well try and learn a new language. On the other hand, those who “braved the rift” and upgraded to 3+ without much hesitation, rewriting crucial modules themselves and adapting the language to their needs instead of the other way around will have a much easier time transitioning to newer versions.
This BC break effectively cut off the lazy and prepared the Python landscape for a new generation of developers. In my mind, that’s a success ushered by a version bump failure. Programming, like so many other walks of life, is still based on survival of the fittest – and if you can’t consume new technologies adequately, get out of the way for those who can.
Applications vs Libraries/Packages
There’s also the questions of apps vs libs. In other words, should the “no legacy” rule apply both to applications depending on, for example, frameworks that get a new release, or should this only apply to libraries in that they should cut ties with previous versions when new ones show up?
Both, in a way.
Libraries that get a version bump to X+1 should follow the clear path to progress – from the moment you have a publicly available new version, maintain a bugfix-only approach on the last one.
Applications that use such libraries are in a more difficult situation due to their logic depending on the APIs that may have changed. The sensible approach is to wait for stability reports from the community and, when verified, to begin the transition. During the transition period, both the old and the new version of the library/framework can remain in use, and once all necessary parts have been upgraded, the old version should be scrapped. Doesn’t this take a long time, though?
There is no web app big enough
“But Bruno, some apps are huge and would take months to be rewritten”, you might say. “Transitioning from ZF1 to ZF2 is a year’s work!”, to which I say: nonsense. There is no web application big enough to warrant that timeframe. In fact, I’ll go one step further and say that there’s no web application big enough to ever warrant using a big framework like Symfony or Zend.
No disrespect to either of those frameworks, they’re hyper complex behemoths of mostly professional code, but if you follow best practices, respect separation of concerns, encapsulate your services and APIfy your application so you can write front ends completely separate from back ends, there is absolutely nothing stopping you from doing full rewrites in under a month, regardless of the app’s size and/or popularity – provided your team mates are coders, and not theoreticians. It doesn’t matter how many patterns and algorithms you know about – you need to know how to use them to be effective enough to do transitions.
Which upgrade scheme should one use then? There is only one acceptable upgrade scheme any software developer should ever use, regardless of app/lib popularity:
- Introduce new branch for new version
- Mark the old version/branch as deprecated
- Publicly state how much life the old version has left
- Issue warning to all users of the old version
- Implement new features in the new branch ONLY, bugfix the old branch/version
- When EOL for old version, cut all ties. Don’t bugfix, don’t consult, don’t even reference it in docs. Kill it.
That’s it – following this procedure will let you never get into legacy trouble.
One project that adopted this approach in a way is Guzzle. They have a 3+ version, a 4+ version for those who can get with the rapid development of the 21st century, and a bleeding edge version for the daredevils who want the latest and greatest upgrades at all times.
As a web developer, I firmly believe legacy code should be abandoned in regards to new features on major version shifts. If you have a large project depending on software that’s moved up a version more than two months ago, you should stop all operations and rewrite everything to respect the new version – especially if the software you’re using is business critical. There is no application in the world big enough to need more than two months for a full transition, and if there is, it should be scrapped and written from scratch – the web is much simpler than all of us let on.
What do you think? Should legacy code be kept around indefinitely? A specific number of versions? Not at all? How do you feel about legacy code in third party projects used by a major project versus legacy code of those third party projects themselves? Do you feel there should be a difference in how either of those handles outdated code? Am I dead wrong here on all accounts? I’d love to have a good discussion about this – after all, my views and experiences are, obviously, my own. Let us know in the comments below.
Bruno is a blockchain developer and technical educator at the Web3 Foundation, the foundation that's building the next generation of the free people's internet. He's also a DX person at Diffbot. He runs two newsletters you should subscribe to if you're interested in Web3.0: Dot Leap covers ecosystem and tech development of Web3, and NFT Review covers the evolution of the non-fungible token (digital collectibles) ecosystem inside this emerging new web. His current passion project is RMRK.app.