I'm a stereotypical Java dev even though I've used many other languages in the past. I think the author missed the point on what Java is designed for. It's supposed to be a friendlier, safer, more general purpose C++ for when you need good but not the absolute best performance. And in this I think its the best thing out there.
The cruft in Java, and hacks like Lombok to reduce it exist in every statically typed language I've used. And if you've worked on a big enough project you'll start to see why dynamic languages like JS and Python are bad choices for enterprise apps.
It's telling that every major dynamic language now has optional typings. Once you get to a certain size the ceremony around access modifier and statically typed objects makes sense.
And the author falls into the common idiom of "the JVM is huge it uses too much ram". Then advocates for dynamically typed languages with even bigger run times that use multiple times more ram. Dynamic languages like Python and JS, as a generality, use 3-10X as much ram as VM languages like Java and C#. They're also equally slower. And the runtime argument makes zero sense. V8 is massive. CPython is massive. Any popular language over time develops a huge runtime.
The author also complains about Spring. Spring isn't Java. There's a ton of Java web frameworks that aren't Spring. No matter how much you like Java you can always find common ground hating on Spring.
Overall the article is a common set of complaints from developers that never used Java or only did a few Spring tutorials, got frustrated at the complexity of the language, and moved on to something else.
Java is one of the fastest popular languages and uses way less ram than many alternatives. And the JVM is a great runtime, not a bloated one. Look at how massive V8 is and the JS standard library is still complete trash compared to Java's. The JVM is possibly the best runtime out there, and a big part of why Java is so popular.
The JVM has great and fast FFI, profiling, metrics, multiple world class GC's, it's rock stable, it optimizes code to within spitting distance of C, it handled projects with hundreds of megs of code with no problem.
Yeah. Java is pretty solid as a language / platform IMO. I gave up on it when Oracle fumbled the ball with JavaFX, but I agree a lot of the criticisms aren't really issues.
The one that gets me is the microservices. I've never really understood it. I can build a Java project with a bunch of small, composable modules and things like refactoring across the entire codebase are trivial. There's no rule saying you can't put your (ex) auth API in a separate module and bundle it independently at build time, right?
Or, if you want to use the same module in multiple projects, formalize the versioning and bundle it as a library.
I think the thing that drives microservices adoption is that it's easy to start out without any planning or API design. You just yeet some code out, publish the REST "API" via Swagger, and call it a "service". There's no versioning, no clear rules, etc.. All of that becomes tomorrow's problem.
Then, since you don't have proper API versioning or anything, you start to end up with things like mono-repos to ensure your "API" changes aren't going to break anyone relying on your "service".
Seriously. Microservices in mono-repos. It's like a bad joke.
Microservices make more sense in dynamically typed languages without a good module system because they turn into spaghetti mess after 20k lines. The worst giant spaghetti projects I've touched were PHP, Perl, and JS. If you can't put boundaries within your app you don't really have a choice but to put them at service level.
In Java (and similar) "enterprise" languages you have static typing, access modifiers, a quality module system. It's trivial to split your app into multiple independent units.
And the JVM + CLR are designed for giant monstrosities. They use a compact bytecode format, zip their dependencies, and do crazy things like runtime code unloading and in-memory string deduplication.
We have Java apps with millions of lines of cruft that run fine on reasonable machines. Totally anecdotal but our worst disasters are dynamically typed stuff like node where the only way to know what the objects being passed around look like is to see them in a debugger. And with JS the "module system" always devolves into webpack concatenating everything into one giant file. It's pretty gross
First of all, Spring is not the only framework out there (even tho I really dislike it). If the title were "What I hate about Spring", it'd be fine, but hating on an entire language because of one framework just misses the point.
> Java’s focus still appears to be on silly rules that dictate what class names should be, what packages they should be in, and if variables should be private or protected. Seriously, who cares?
This is the one thing that I like about Java. I can easily guess the names of classes/methods. Private variables are private so that they don't leak into the public API (ie. encapsulation). The "seriously, who cares?" at the end just discredits the entire article for me.
I agree it's not the Java language per-se that is messed up, its the runaway use of annotations in frameworks and libraries that makes building an application a write-once never be able to debug problem. The real problems start when you start work on a existing codebase that you are not familiar with. Deep expertise with Spring is an absolute requirement for even minor work. That's what puts off management from investing in Java for new projects.
If there's something worth criticizing it's just how large and non-obvious it is to figure out what Spring(/Boot) does. And before you know it you're digging into some PostProcessBeanFactoryInitializerDooDad.
I've come to appreciate some aspects, but it's not obviously good to me, like a lot of stuff it's just various tradeoffs.
I like Java and the surrounding ecosystem/community because it's boring.
> The "seriously, who cares?" at the end just discredits the entire article for me.
It doesn't for me. I can't keep track of all the times I've been frustrated by a "final" class or a private variable somewhere that, were it public, I could have trivially solved my urgent business problem in time, with full awareness of the tech debt being incurred.
But I can't remember any time in the tens of thousands of lines of Python that I've written, where I had regretted consuming a variable somewhere.
So at this point, based on my experience, it all just sounds like paranoia to me that doesn't actually add value in practice. I think many of these "best practices" in Java, especially those coming from "Effective Java", are over-paranoid for most developers, and just create more problems than they solve. Being paranoid about API contracts makes sense if you are writing a widely-consumed library like those that Josh Bloch worked on, where the cost of changing consumers is prohibitively high. But when you are working on a class that has a handful of consumers, it really doesn't matter--just go update the consumers if you really need to (which you probably don't).
> I can easily guess the names of classes/methods.
And if you're using something like IDEA it's even better. You can take a guess at the method name, look at the auto-complete options, pick one, and hit CTRL-Q to read the Javadoc for that method.
Joke aside, while I don't like typical Java frameworks' complexity and haven't done any Java in seven years for that reason, calling java dead because “everybody is using microservices” and “why bother with performance when you can just run more workers” is a bit ridiculous.
First of all, in practice “everybody uses PHP” is still way closer to the reality. The microservices hype train is real, but it's far from ubiquitous in real life and the fashion will probably be dead long before Java (I'm not saying microservices are bad per se, but it's way overused, by companies who have zero scalability issues and adds a lot of engineering complexity).
Then there's cost: deploying 3 times more machines because you use Python on a high traffic service is a good way to give some margin to the cost-killers in the next LBO.
My company had a massive, over-engineered monolith that we broke down into more sensible microservices, and the architecture is now much cleaner and easier to maintain, scale, etc. I agree, though, that designing as microservices up front is generally premature optimization, as your initial guesses for where to put the boundaries between the services is likely to be wrong.
Both the monolith and the microservices are written in Java with Spring Boot, but I agree with the author that Spring especially leads to runtime code that is extremely difficult for developers to really understand. I am curious whether writing future microservices in a language like Go might be more maintainable in the long run.
I would not agree that Go is a good replacement for Java Microservices. I would suggest looking into something like Quarkus (https://quarkus.io) and if Java is to verbose for someone, she/he can checkout Kotlin (https://kotlinlang.org) which is 100% interoperable with Java but clean, modern and pragmatic. With Quarkus you have a very powerful but simple framework that out of the box (GraalVM) can be compiled also to a single binary if you need very fast startup and very low memory consumption.
Go evangelises simplicity but when someone has to use it in a bigger project the lang is very cumbersome. You don't have any functional programming features like filter, map, find to manipulate collections. The not null (nil) safety or the magic with interfaces{} does it make non safe especially compared to modern languages like Kotlin,Swift which have the notion of nullability build in the language itself. Dependency Injection support is very limited. I think Go is hyped because of the excellent single binary feature which you have nowadays via GraalVM also for Java, Kotlin as I described above.
That's the thing though. If you have an overengineered or spaghetti-code monolith, often you can get management buy-in to rewrite it by saying "microservices"! Likewise, if you have an over-engineered or spaghetti-code set of microservices, often you can get management buy-in to rewrite by saying "microservices were a fad, we need tighter integration".
The style you rewrote in didn't solve your problem, the act of rewriting did.
> My company had a massive, over-engineered monolith that we broke down into more sensible microservices, and the architecture is now much cleaner and easier to maintain, scale,
5 years from now you will go from a massive, over-engineered micro service architecture. To a well designed modular monolith that is much cleaner and easier to maintain, scale, etc.
> Simpler code base from not having to worry about concurrency and multi-threading
Microservice advocates are so naive it's painful. It is only simpler if you completely ignore the downsides of microservices. Taking a "monolith" application and turning it into a distributed application rarely makes it simpler. Now you have to deal with network failures, serialising and deserialising data for what was previously just a method call, API versioning all over the place, kubernetes/terraform/etc configs from hell, concurrency and race conditions still persist but now they're spread across services instead of between threads. You want data consistency and transactions? Forget about it!
By mentioning data consistency and transactions - your microservice is supposed to be self-contained and self-sufficient, doing its own job only. True, it may rely on other service, but not in the way that the two services would do the exact same job together as if you're invoking a `private` method helpers.
For ex., filling a shopping cart should be its own thing. Searching for stuff to fill that cart can be another service that is just a dumb catalog. No data consistency problems here.
OK, so when the catalog service is updated I need to update the search service and the cart service and coordinate deployment of these various services, or provide multiple versions of the catalog api.
Yes. If you have several consumers of the catalog service, those consumers are bound to a specific version it. You may update the catalog service API as much as you wish, but that will simply open /v2, v3, .../ endpoints - existing services shouldn't be affected. Just like with regular REST APIs. Deprecation of older API versions will be as disrupting as expected, of course. If existing consumer of catalog service needs to make use of the new version features, you update it on its own...
Version hell? You can imagine the horrors. But technical debt on an architectural level is something you need to consider any architecture you choose to create your application in.
Point of micro services is have your components loosely coupled and independent from one another. If catalog fails, it shouldn't bring down the whole application with it. In our simple example, catalog failure means whole app becoming inert, but in bigger applications you _may_ find benefits of this.
Also worth mentioning - load can vary between components. The cart will see less use than the search service, while the catalog will be most loaded. You can make deployment decisions to optimally allocate resources for your application.
The article isn't about Java dying. It's about the author's gripe with Spring.
Undoubtedly, 10 to 15 years ago he'd write and article about how Java is dying because Struts.
20 years ago it would be an article about how it's dying because there's nothing to compete with the new kids on the block in the web space.
Java isn't going anywhere. It will probably slowly lose its significance over a few years, but it will be a decade or so before it will actually start dying (if at all).
You will not have a choice in the matter. Most large existing projects already use Spring. Changing those to not using spring is a recipe for getting yourself fired.
I have not used Spring since 2009 (where I used it daily) and have been using Java full time throughout. In that time I have written a lot of code and worked with many excellent teams. I’ve not had any of your problems because we always chose tooling that, for us, struck better balances and gave clear, concise error messages. There are certainly projects which are badly designed, but that is the team’s failing in every one of your complaints. Perhaps you should look inwards and not lash out against a large, diverse community.
> Simpler code base from not having to worry about concurrency and multi-threading
This kind of thing irks me when I read it. I don't understand why people think of microservices as an architecture pattern. To me they seem more like an implementation detail.
When it comes to concurrency and multi-threading, microservices mean you have different problems to worry about, not that they go away. I try to get people on my projects to imagine microservices as network function calls. If you diagram out how your data flows, how your concurrency works, you tend to see that a microservice is roughly a thread in a monolith. You gain not worrying much about mutexes or shared memory, but you lose on performance. Languages like Rust let you get close to having your cake and eating it too.
It is also a useful exercise for teams to consider "how big does my service need to get before microservices on Python scale better than a 64-core monolith in C/C++/Java/Rust?" Another good question, "if a monolith is stateless, does it scale as well as microservices?"
>This kind of thing irks me when I read it. I don't understand why people think of microservices as an architecture pattern. To me they seem more like an implementation detail.
Implementation details and architectural patterns are orthogonal.
Implementation detail means something that is-not/should-not be exposed at higher levels (e.g. API consumers and/or the end user). This includes architecture, but also all kinds of other things (e.g. choice of storage backend).
Microservices is very much an architecture pattern (affecting the whole architecture/design/code-base of the app in the most fundamental way), and is/can-be an implementation detail, as far as API/UI users are concerned.
Spring is completely undebuggable. If you get your annotations wrong, you simply cannot debug the process. I'm desperately look for tools that can give insight into what objects were auto wired at runtime.
It's trivial to observe runtime behavior with a debugger.
If you use constructor injection as it is recommended, just add a breakpoint in the constructor and start the app in the debugger.
@Component
public class Foo {
public Foo(Bar bar) {
// breakpoint here
}
}
Edit:
In most cases it should be obvious what is getting injected, e.g. in the example Bar is another component/bean that was manually created. If you have two beans of the same type, use a qualifier to specify which one you want.
If you are having issues with unexpected things being injected, I'd recommend setting up a few tests that establish that your app is properly wired up.
Much as I wish Java would just die sadly that's not the case as far as business goes. There is probably more money changing hands for Java worldwide than any other language and with new languge features appearing it doesn't look likely to change anytime soon.
Agree with everything he said about Spring especially, but was surprised he advocated Python as the alternative, not Go.
Still getting up to speed on Go, but it seems a more straightforward replacement for Java in a microservice context. More performant than Python, trivial to deploy, and provides a good amount of static type checking.
I’ve written a ton of microservices in Java and have recently started a new project using a very similar architecture, but with Go.
I had a chance to reflec on what I like about Go, and what I miss from Java. To boil it all down:
* I prefer the Java language over Go, for a bunch of reasons, although neither is perfect.
* I very much prefer using binaries over bytecode objects, so Go is much better here.
* The JVM is a very large runtime to associate with your code, and the startup time is still much longer than for Go (or it was a year ago). This makes Java CLI tools slow, and a great CLI can help make a great product.
* I prefer Go’s approach to the standard library; easy to use production ready “for dummies” implementations, instead of abstract “reference” implementations.
* I specifically don’t like Go’s use of capitalisation to mark fields and variables as public, since I find that collisions between type and variable names are really common.
* Go’s “idiomatic” practices are much saner than Java. I’d already stopped using getters and setters in Java, along with dropping several other abstract “best practices” (factory factories!!?? “Fluent” APIs??), and Go really embraces a light approach here. So I guess I don’t really like idiomatic Java. I got the feeling I wasn’t alone, tho.
* In general I like how Go feels like it’s a Unix tool, where Java always felt like they were trying to rewrite the world. This approach always felt odd to me, considering where Java came from.
* I have really come to hate the “everything’s a framework” approach in Java and abandoned it a couple of years ago. Especially, Java EE, Wildfly, Spring, JPA etc. they all add enormous weight and complexity without realising any material benefit. It’s not that it has to be this way, and I think things were improving, but so many Java libraries were just massively complex and abstract, for no good reason.
So for my use case, Go is clearly the right choice, but after 12 months working in Go there is plenty that I miss about the Java language. I had really high hopes for the Java AOT stuff but it seemed to stall (maybe I missed something).
> I had really high hopes for the Java AOT stuff but it seemed to stall (maybe I missed something).
Project Leyden [1] aims to standardise AOT for the Java Platform. It is planned to start sometime in the new year. We will most likely end up with two implementations of it. One is GraalVM's native image which will most likely need to make some changes in order to follow the specification. And another implementation will likely be one that is based on HotSpot's C2 compiler [2][3].
I agree with pretty much everything you said except this one:
> In general I like how Go feels like it’s a Unix tool, where Java always felt like they were trying to rewrite the world.
Go is pretty much on par with Java when it comes to rewriting the world albeit not in the same way: they have their own assembly, their own linker, and even have their own libc using raw syscall (even doing so on windows and Mac for a while, which caused issues because there is no syscall stability there). And since it has a poor FFI, many Go developers end up rewriting their own libs in Go for pretty much everything when they need one lib.
Yeah - I agree, and I suppose given the history and people who wrote Go, I tend to give it a pass here where maybe I shouldn’t. I also don’t tend to do any work in C any more so haven’t noticed that. I would say though that Go library calls work more like Unix/stdlib calls than Java, which makes abstractions out of even the most basic stuff, like filenames.
But my point was more that, relative to Java, the performance and CLI tools feel very Unix like, they remind me of my C programming days. I mean Javac/mvn/gradle are all so slow relative to Go; Java command line tools don’t really seem to care about developer ergonomics. It’s hard to imagine chaining a bunch of Java CLI tools together with pipes!
This is more because of the long startup times and cost of the JVM, but that too is part of the problem.
I would not choose Go, the language is simply to simple and not modern at all. You will still have to check for nil values ( see Kotlin, Swift for alternative) or you have to use old for loops to manipulate cumbersome collections types like slices instead of having functional paradigms build into the language ( again Kotlin, Swift, Java Streams etc). A good alternative if already using JVM would be some of the newer generation frameworks like Quarkus (https://quarkus.io), Helidon etc
The cruft in Java, and hacks like Lombok to reduce it exist in every statically typed language I've used. And if you've worked on a big enough project you'll start to see why dynamic languages like JS and Python are bad choices for enterprise apps.
It's telling that every major dynamic language now has optional typings. Once you get to a certain size the ceremony around access modifier and statically typed objects makes sense.
And the author falls into the common idiom of "the JVM is huge it uses too much ram". Then advocates for dynamically typed languages with even bigger run times that use multiple times more ram. Dynamic languages like Python and JS, as a generality, use 3-10X as much ram as VM languages like Java and C#. They're also equally slower. And the runtime argument makes zero sense. V8 is massive. CPython is massive. Any popular language over time develops a huge runtime.
The author also complains about Spring. Spring isn't Java. There's a ton of Java web frameworks that aren't Spring. No matter how much you like Java you can always find common ground hating on Spring.
Overall the article is a common set of complaints from developers that never used Java or only did a few Spring tutorials, got frustrated at the complexity of the language, and moved on to something else.
Java is one of the fastest popular languages and uses way less ram than many alternatives. And the JVM is a great runtime, not a bloated one. Look at how massive V8 is and the JS standard library is still complete trash compared to Java's. The JVM is possibly the best runtime out there, and a big part of why Java is so popular.
The JVM has great and fast FFI, profiling, metrics, multiple world class GC's, it's rock stable, it optimizes code to within spitting distance of C, it handled projects with hundreds of megs of code with no problem.