Thirty years of Java, and I've been there for most of it

I've spent most of my 25+ years shipping systems on a language people keep predicting will die. Java just turned 30, and I wanted to write down why it's still the backbone of the systems I build today.

I started writing Java professionally back when applets were still a thing people took seriously. That alone should tell you how long I've been doing this. 25+ years of backend work, and Java has been the constant thread through almost all of it — from Bank of America's digital marketing stack, to hospital microservices on AWS, to the platform I'm building right now at ITJobOpportunities. So when people ask me why I still bother with Java in 2026, when there's Go and Rust and whatever flavor of the month is trending on Hacker News, I usually just tell them the language has survived four major architectural eras and it's still standing. That's not nothing. I want to walk through where this language came from as someone who lived through most of the middle chapters and can tell you what actually mattered versus what was just marketing. The Sun years: bytecode as a bet James Gosling and his team at Sun Microsystems started this thing in 1991 under the name Oak, aimed originally at consumer electronics. It was renamed Java in 1995 and released publicly that same year. The pitch was "write once, run anywhere," which for the era was a genuinely wild claim. Most languages compiled straight to machine code tied to a specific OS and chip architecture. Java compiled to bytecode running on a virtual machine. That decoupling is the single most important architectural decision in the language's history — the garbage collection, the JIT compiler, the whole ecosystem exists to make that bet pay off. I didn't touch Java until a few years after that initial release, but I remember the applet era vividly: little programs embedded in web pages, running in the browser through a plugin. It felt futuristic. It also broke constantly and was a security nightmare, and browsers eventually killed applet support entirely. Nobody mourned that. What actually stuck from that first decade wasn't the client-side story — it was the server-side one. Servlets, J2EE, and the enterprise gold rush Java's real home turned out to be the server. Servlets arrived in 1997, then J2EE in 1999, bundling EJB, JMS, JDBC, and a pile of specifications that promised to solve every enterprise problem imaginable. This was the era of massive application servers: WebLogic, WebSphere, JBoss. Deploying a change meant a WAR file, an app server restart, and usually a prayer. I came up through exactly that world. Early enterprise work meant hand-writing EJB 2.x entity beans with deployment descriptors longer than the actual business logic — home interfaces, remote interfaces, local interfaces, all boilerplate, all painful. Spring Framework showed up in 2003 specifically as a reaction to how heavy J2EE had gotten, and it's not an overstatement to say Spring saved the Java ecosystem from itself. Dependency injection without the ceremony. POJOs instead of beans that had to implement six interfaces just to exist. I still remember ripping an EJB layer out of a legacy system and replacing it with plain Spring beans wired through XML config, before annotations took over. The line count dropped by roughly 60%. That was the moment I stopped thinking of Java as "the verbose enterprise language" and started seeing it as something you could actually build fast with, if you picked your tools right. The lost years: Java 6 to Java 8 Java 6 shipped in 2006, and Java 7 didn't land until 2011 — five years. Oracle had acquired Sun in the meantime, and there was real concern in the community about whether Java would stagnate under new ownership. It was not a fun stretch to be a Java advocate. Then Java 8 landed in 2014, and it's genuinely one of the best language releases I've worked with. Lambdas. Streams. Optional . Functional interfaces. It didn't turn Java into a functional language, but it gave enough functional tooling to write dramatically cleaner code for the everyday work of transforming collections and handling nullability. List<String> activeUserEmails = users.stream() .filter(User::isActive) .map(User::getEmail) .collect(Collectors.toList()); Compare that to the for-loop-with-null-checks version everyone wrote before 2014, and you'll understand why so many shops stayed on Java 8 for close to a decade. I had clients running production services on Java 8 well into recent years — not out of laziness, but because it worked, and the upgrade path to 9+ (Project Jigsaw's module system, in particular) wasn't obviously worth the migration cost for a long stretch. The six-month cadence, and why it actually matters Oracle switched to a six-month release cycle starting with Java 9 in 2017, then designated certain versions as Long Term Support: 8, 11, 17, 21, and now 25. Instead of waiting years for the next big thing, you get incremental releases constantly and pick your LTS version as your production baseline. I was skeptical of this cadence at first. It felt like churn for its own sake. Looking back at what shipped in each LTS since then, I've come around completely: Java 11 (2018): Built-in HTTP client, var for local type inference, a bunch of smaller cleanups. Solid, unglamorous, dependable. Java 17 (2021): Sealed classes, maturing pattern matching, and records becoming a real production tool for DTOs. I lean on records constantly to cut Lombok boilerplate for simple data carriers. Java 21 (2023): Virtual threads (Project Loom) shipped here, and it's the one that actually changed how I architect services. Java 25 (2025): Continued refinement on virtual threads, structured concurrency moving toward finalization, better pattern matching for switch. This is what the ITJobOpportunities backend runs on today. Virtual threads deserve their own paragraph. For years, the standard answer to "how do you handle high concurrency in Java" was reactive programming — WebFlux, Mono and Flux everywhere, callback chains that were hard to debug and harder to onboard new engineers onto. Virtual threads let you write plain, blocking-style code and still get the throughput benefits, because the JVM handles scheduling under the hood instead of tying up an OS thread per request. try (var executor = Executors.newVirtualThreadPerTaskExecutor()) { for (Candidate candidate : candidates) { executor.submit(() -> resumeService.scoreAgainstJob(candidate, job)); } } On our Job Fit scoring pipeline at ITJobOpportunities, this pattern let us fan out LLM scoring calls across a batch of candidates without wiring up a reactive stack just to avoid thread exhaustion. It's not magic — you still have to watch for thread pinning on synchronized blocks or native calls — but for that use case it removed a lot of complexity that used to live in .subscribe() chains nobody wanted to maintain. Spring's parallel evolution You can't tell Java's enterprise story without Spring's. Spring Boot arrived in 2014 and did to Spring configuration what Spring did to EJB: stripped out the ceremony. Auto-configuration, embedded Tomcat, opinionated defaults, a single @SpringBootApplication annotation replacing pages of XML. Standing up my first Spring Boot service, I remember being almost suspicious of how little config I needed. Spring Boot 2.x carried most of the industry through the Java 8 to Java 11 transition. Spring Boot 3.x, requiring Java 17 minimum, moved the ecosystem onto Jakarta EE namespaces — the javax.* to jakarta.* rename after Eclipse Foundation took over Java EE from Oracle — and that migration was genuinely annoying for a lot of legacy codebases. I've done that migration on more than one engagement; it's mostly mechanical, find-and-replace with import sorting, but on a big enough codebase with dependencies still stuck on javax , it's a real slog. Spring Boot 4.1 is the current frontier as I'm writing this — I put together a sketchnote on the release highlights because there's enough there worth visualizing: tighter integration with virtual threads, continued push on observability defaults, better HTTP interface client patterns. Th...