My Issues with Java
Java is mostly a fine language. But I want to describe the issues I see with Enterprise Java development.
I don’t personally write a lot of Java. Only when I’m working on an existing project. But in my role, I spend a lot of time troubleshooting performance problems, memory leaks, and unintended behavior in applications frequently.
This is somewhat of a rant. A therapeutic exercise.
Java, the language and even the JVM is fine for what it is. It is completely possible to write a sane, performant application with it and the tooling around it.
I’d even advocate learning Java: the good, the bad, and the ugly. Every language or framework I’ve picked up has taught me lessons and different ways to approach a problem. Some of those are positive (as in how to), and some negative (as in how not to).
My issues here will go into the problems with what I see in practice, particularly in “Enterprise Software Development.”
Since the late 90’s when the Java fad landed and became the primary language taught in computer science departments, we’ve seen this trend:
- There are more jobs in Java development so more people learn Java to get a job.
- There are more people that “know” Java, so it’s easier to hire a Java developer (at a lower rate) than any other language.
- GOTO 1
OO and Design Patterns
This is not particular to Java by any means. It really applies to any language in the C++ inspired family: C++ itself, Java, C#, etc.
Firstly, the dream of OO never panned out. And it was never practical. The idea of reusable self-contained software components just never landed. I’ve rarely encountered effective inheritance in real life. Add to that the complexity that landed between inheritance, abstract classes, and interfaces. It’s an easy pitfall to use the wrong capability for the wrong purpose.
Some libraries really do it well. But for business applications 99% of code is 1-off. And worse, it’s full of defensive programming for situations that will never be encountered. Lines and lines of complexity providing no value.
Public/private attributes are almost never leveraged. What if you wanted to do something with those values? So we’ll always use setters and getters, when 99% of the time it’s just boilerplate. We have IDEs just generate that stuff for us now.
I do have to say, Python’s properties shine here. When you do find yourself in this situation, you can make setting and getting perform logic without changing the interface. I’ve probably used them a handful of times.
I’ve seen many developers make an interface and/or an abstract class for everything “just in case”. It’s a “best practice”. Mix in inheritance, and now you have to look at 5-6 files just to derive the basic business logic.
This is a maintenance nightmare, and makes troubleshooting performance problems a pain in the ass. The vast majority only have one (concrete) implementation in their entire life.
Again, this isn’t limited specifically to Java. You see it in most OO languages, and some now (eg Kotlin) are trying to reduce this burden.
I’ll be honest. The only Gang of Four I love is the band that made Entertainment!. Again, this was a well intentioned idea gone awry in practice.
So a set of patterns turns software from being ad hoc to engineering. You don’t build a bridge from scratch. You use well known and understood patterns to build a reliable piece of infrastructure.
In reality, though, Design Patterns are too abstract, poorly understood, and honestly, not clear to the average developer when to apply them.
“Should it be a Builder or a Factory?” is one of the most common mistakes I see. And often times, the answer is “neither.”
Again, the theme here is frequently defensive design creating unnecessary complexity providing no value.
Within a modern business application, there’s typically 1 team, and the actual “interface” is the API. Making changes within the code doesn’t carry the burden that provides value to that complexity as long as the API is delivering on its contract.
Enterprise frameworks such a JEE and Spring and their magic often result in something akin to “undefined behavior” in C. Too many times I see a team that has made software that they believe does one thing when in reality it does another. Configuration wiring together code, and Inversion of Control makes the end-to-end picture of what the software does completely opaque.
Is that code running where and when I think its running? When is that injection occurring? At startup? At instantiation? When is that instantiation? Per-request? Per-session? For the lifespan of the process?
They relieve some of the boilerplate I complained of above for sure. But too many times I run into software where the developers believe one thing is happening and it’s something else entirely.
This may not be limited to Java. It may be a product of our “copy and paste from Stack Overflow” development culture.
But to the above, very few developers understand what is actually happening under the hood. They type or paste, it doesn’t work. Then they futz with it, it “works” and they move on never to think about it again.
Almost none understand the JVM itself. Stack traces and heap dumps are mysterious without hand-holding. The GC algorithm changes are just whoosh.
I still run into “This has worked fine for 15 years why is it running like trash on Java 8”. It was written in Java 6, was barely getting by, and the GC improvements in Java 8 whacked them. No profiling is done. Java Mission Control is frankly an amazing facility, and sadly, almost never used. And I see sooo many Java applications being passed deprecated GC arguments emitting a warning every time they start. But it works, so whatevs.
The asynchronous improvements are killing me. Too many applications that have a concrete SLA for response time are using it because “it is faster.” Then load hits, the app scales to throughput well at the expense of response time. Why is it so slow? It must be the infrastructure.
Making necessarily synchronous calls to necessarily synchronous activities is usually a root cause. HTTP and database calls back up, the app starts thrashing between threads and CPU spikes from 10% straight to whatever it can get. It’s water buffalo creating a bridge of drowned/trampled water buffalo across a creek.
So it’s an incident: “scale scale scale!” Soon a simple app is taking up a multi-million dollar hardware footprint.
Being synchronous isn’t “bad” or “slow”. Sync and async are just tools in the bag to be used appropriately. But when your apps are doing massive transaction throughput across a 40GB LAN link, accounting for internet I/O is silly and results in unpredictable behavior.
And so many time apps use REST, where they should be messaging and vice versa. Databases are full of tables with no relationships and 2 columns where a proper cache would serve them better. Redis is used as a messaging system when proper JMS systems are available. I have literally seen a set of “microservices” use REST for IPC, a database as a key/value cache, and Redis for messaging.
As Tricky said, Makes Me Wanna Die.
There is actually an OS under there
With Java abstracting the details of the OS, it’s easy to forget it’s actually down there.
Write one, run everywhere is amazing for the set-top boxes, etc Java was designed for.
But in reality, it’s usually used for Enterprise software running on a consistent Linux/x86-64 stack. The abstraction isn’t particularly useful.
I’m from the last generation of people that learned C in college. And definitely before curriculums became focused on churning out developers “ready for the job” like a trade school vs focusing on CS fundamentals and using Unix as the example.
They don’t know the C standard library. Or syscalls, context switching, TCP/IP, DNS, etc. It’s voodoo. And when my app is slow, the answer is more hardware (vertically or horizontally). And so often it’s for an app that just takes input from JMS or HTTP, changes a few things, stores it in a database, and passes it along to the next system. It’s not doing ML. It doesn’t need huge datasets in memory. But the dev doesn’t even know where to look.
Procedural isn’t terrible
I often wonder if Enterprise Developers were forced into more procedural languages would code quality and performance improve? Would they understand their software better?
I think there is an advantage to being able to “walk” the behavior better beginning to end, vs chunks of code layered on each other magically wired into an abstract system.
C is a deathtrap. The chance of security bugs is too damn high with the pace required to continually deliver features, and with a workforce of mostly “mid” talent. I have no illusions there.
And as much as I love Python, Ruby, and the like, dealing with the volume many of these need to is too difficult or impossible.
While definitely not procedural, there’s a lot of potential for these systems to be built in something like Erlang. But I honestly cannot image the average developer to be able to wrap their head around that.
I wonder if Rust would be too complex for “business”? That said, Java itself is a very “large” language and just getting larger. So many keywords, facilities, and features. It’s a lot OO, but more and more functional features are being added and the async work is adding more to that. Maybe it would be fine.
I think Go it great for that level. It’s a simple language. A solid concurrency model. It’s GC is on par with and as safe as Java. There aren’t as many sharp edges. Most errors are caught in compilation, and it’s hard to write code that doesn’t work as intended.
Maybe I’m Wrong
But are those advantages because they are more niche and therefore still the domain of less “mid” devs? Do I see better software in those communities because it is created by people seeking to learn vs just work.
Is this all just because the talent pool for talented software developers is not nearly large enough to meet the demand? And whatever tool is used, few would understand what is happening because they just don’t try.
Maybe I’m just doomed to late-night sessions troubleshooting poor systems. And maybe I should be thankful for Java’s mature profiling and troubleshooting facilities to help me along the way.
Maybe I should just dream that one day teams will actually fix the problems we identify in a reasonable time frame and we won’t just throw hardware at the problem. Maybe it’s not the language or the Enterpriseyness at all.
Well, at least I have a job, I guess.