When and Why to Drop Language Versions
by Nate Harris
When the Developer Experience team was first established here at EasyPost just eight months ago, one of our first milestone goals was to tackle our inconsistent programming language support. For example, our C# client library was still mostly built for the legacy, Windows-only .NET Framework despite the introduction of the in 2016. Our Python client library still had Python 2.7 support over two years after . Our PHP client library was still supporting PHP 5.3 as a minimum a whole seven years after that version was .
After bringing all our client libraries up to feature parity with a variety of minor releases, we decided to set a cutoff on support for older language versions. We bumped our minimum support levels and forged ahead with new major releases for all our client libraries. As my colleague Justin , we wanted to strike the right balance between supporting older language versions for long-term support and adopting the newest languages for the benefits they often bring.
Resistance to change
As anyone working in technology probably knows, for as many people that are always clamoring to adopt the newest framework and features, there are just as many people reluctant to upgrade until they are practically forced to. There is a "if it ain't broke, don't fix it" mindset that is prevalent in our industry. This mindset often results in developers clinging to outdated versions of languages well past their prime. In the case of Python 2, many of the most prominent Python packages and programs retained support for the old version up . Some even went beyond the sunset date, and some have decided they will simply for the dead language. Meanwhile, Java 8, the oldest, active version of Java, was released in 2014 and only just fell out of "Premier Support" in March. It is in "Extended Support" until the end of 2030, at which point the release will be 16 years old. And yet, due in part to its LTS status, Java 8 continues to be the most common Java version used by our EasyPost customers.
Needless to say, there is a lot to consider when we do decide to change support levels for our client libraries. To make the best decision possible, it is important to consider why we need to adopt a new version of a language and/or drop an old version.
What motivates an upgrade
Thankfully, nowadays, most of the programming languages we use for our client libraries follow a fairly consistent release pattern. Node.js, Java and Go usually release a new version every six months, while Python, .NET, Ruby and PHP opt for yearly updates. Some of these languages, like Node.js, Java and .NET, consider each new release a major version bump, while others consider it a minor version bump, saving major version bumps for less frequent and more seminal changes. Nevertheless, each new version typically comes with its own set of (purportedly positive) new features and improvements.
Prior to Java 8, several years would typically pass between each version. This means more new features, and more breaking changes, were in between, say, Java 6 and Java 7, than between Java 14 and Java 15. The higher frequency of releases of any given programming language means less time for developers of that language to decide to redesign things. In theory, that means less breaking changes and more backwards compatibility, and an overall easier upgrade experience for the end-user. As we know, it is already difficult enough to convince some developers to take a chance on a new language version. That gets even harder when the upgrade path is rough and requires major refactoring.
Oftentimes, there needs to be something particularly enticing to motivate a developer to upgrade to a new version. Python 3.10, for example, introduced its much anticipated , a feature most other languages had but Python had always lacked. .NET's cross-platform overhaul, meanwhile, opened up a whole new world of C# development no longer tethered to Windows-only ecosystems. Sometimes, if a developer has held out for long enough, it is not any one particular feature, but a deluge of improvements that collectively make an upgrade alluring. And, of course, there is an expectation that each new version of a language will patch existing bugs and vulnerabilities, making an upgrade worthwhile from a security perspective.
Build for the lowest common denominator
We find ourselves in a difficult position because we are not making an end product. Developers making their own full-stack applications have more autonomy over what version of a language they decide to use, since they themselves are ultimately the ones affected by the decision. For us, we don't have control over the ecosystems in which our code will be used, meaning we can't blanket-decide what version of a language our code will run in. Instead, we have to make assumptions about what other developers will want to develop with our code, and with what language version. We are beholden to the developers who want to use our client libraries in bleeding-edge environments just as much as we are to the developers who want to use them in legacy applications. And that is where the decision gets tricky. Thankfully, the developers and organizations in charge of the programming languages often provide guidance on language version support.
Nearly every new language version arrives with an expiration date, often signaling how invested a developer should be in the new version. Some new versions arrive with only left before they're considered defunct; others, particularly LTS versions, are not set to expire for nearly a decade. We typically hang our hats on LTS releases, with the expectation that most of our end-users likely will too. As a result, we generally plan to support LTS versions of languages as long as we reasonably can.
Clinging to LTS versions does sometimes have its drawbacks. Programming languages change for a reason, and purposefully abstaining from new features sometimes can make things more difficult. For example, Java 11 that is far and away an improvement over the legacy client used in Java 8. Unfortunately, since we have decided to support Java 8, we cannot utilize this new client and must instead use the legacy client. And since all our Java versions use the same code base, built for the lowest common denominator (Java 8), even our Java 18-compatible JAR files are using this legacy code.
When to call it quits
We do not live and die by the LTS rule, however; sometimes we will prematurely drop support for LTS versions when the evolution of the library outweighs the demand for backwards compatibility. This is true of our .NET library, where we recently decided to drop support for .NET Framework v3.5, which does not reach end-of-life until 2029. Without getting into Microsoft's confusing .NET Framework/Standard/Core versioning and rebranding matrix, effectively .NET Framework v3.5 is the only active LTS release of .NET Framework, with some more recent versions having already reached their EOL dates. The problem is that the entire .NET Framework is slowly dying off; .NET Framework v4.8, released in 2019, will be ever. Because of this, many applications and extensions for .NET are steadily dropping backwards compatibility for .NET Framework and instead exclusively supporting the newer cross-platform .NET/.NET Core.
Two major changes to our .NET code base effectively forced our hand on support for .NET Framework v3.5. The first was asynchronicity. In version 3.0.0 of our EasyPost .NET client library, we made all our function calls asynchronous, which that shaved literal seconds off execution times. The problem is, asynchronous functions in .NET until .NET Framework v4.5. The second change was RestSharp, an important dependency that handles the actual HTTP calls in our client library. We had received notice from our .NET users that our library, which was using RestSharp v106, , many of which were using RestSharp v107 for other purposes. v107 had dropped support for .NET Framework, but at the same time, had , meaning we could not get away with simply using the same base files for all our supported .NET versions. We briefly considered the idea of splitting our code base into two separate bases, one for .NET Framework and one for .NET/.NET Core. However, as we began introducing new features, including implementing a , the support matrix began to balloon and we vetoed the concept.
Instead, we decided that we should not be holding back our library's ability to evolve simply to maintain compatibility with a LTS version of a soon-to-be-deprecated framework. The calls from our community and the dramatic performance improvements far outweighed the value of preserving wide-reaching compatibility. So, we jumped the gun a few years early and . We still support all other active versions of .NET Framework via .NET Standard compatibility, though, which we see as a fair compromise. And if that does not work, users can always simply refuse to update and continue using the old release, with the understanding that they will be missing out on new features.
Beholden to progress
There is something to be said about the role we play in our partnership with our users, and the influence we can potentially wield. There is an understanding that we have no place in telling our users what they can and cannot do with their systems. It is none of our business how they operate; we simply do our one thing, making it dead-simple to buy postage and ship packages with EasyPost. At the same time, we have an inherent responsibility as technologists and stewards of programming to provide the safest, most efficient, most up-to-date software. And, well, you can't make an omelette without breaking some eggs. Sometimes it is best to just meet people where they are, even if that means supporting outdated versions of a language; other times, it is on us to perhaps be that guiding force that finally pushes a developer to stop using deprecated software.